@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
|
@@ -61,7 +61,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
61
61
|
it('serializes query parameters', async () => {
|
|
62
62
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
63
63
|
await api.getMany({ page: 2, perPage: 5 })
|
|
64
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
64
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
65
65
|
expect(url).toContain('/service-access-management?')
|
|
66
66
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
67
67
|
})
|
|
@@ -76,7 +76,7 @@ describe('ServiceAccessManagementApi', () => {
|
|
|
76
76
|
it('returns unpaginated array when paginate: false', async () => {
|
|
77
77
|
vi.mocked(client.get).mockResolvedValue([record])
|
|
78
78
|
const result = await api.getMany({ paginate: false })
|
|
79
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
79
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
80
80
|
expect(decodeURIComponent(url)).toContain('"paginate":false')
|
|
81
81
|
expect(result).toEqual([record])
|
|
82
82
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ServicesApi } from './ServicesApi'
|
|
3
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
+
import type { ServiceLimits } 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
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe('ServicesApi', () => {
|
|
23
|
+
let client: ReturnType<typeof createMockClient>
|
|
24
|
+
let api: ServicesApi
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
client = createMockClient()
|
|
28
|
+
api = new ServicesApi(client)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('getLimits', () => {
|
|
32
|
+
it('calls client.get with /services/limits', async () => {
|
|
33
|
+
const limits: ServiceLimits = {
|
|
34
|
+
whatsapp: { used: 1, total: 80, available: 79 },
|
|
35
|
+
telegram: { used: 0, total: 0, available: 0 },
|
|
36
|
+
}
|
|
37
|
+
vi.mocked(client.get).mockResolvedValue(limits)
|
|
38
|
+
const result = await api.getLimits()
|
|
39
|
+
expect(client.get).toHaveBeenCalledWith('/services/limits', undefined)
|
|
40
|
+
expect(result).toEqual(limits)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('forwards custom headers', async () => {
|
|
44
|
+
const headers = { 'X-Custom': 'value' }
|
|
45
|
+
await api.getLimits(headers)
|
|
46
|
+
expect(client.get).toHaveBeenCalledWith('/services/limits', headers)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { ArchivableCrudApi } from '../../core/ArchivableCrudApi'
|
|
3
|
-
import type { Service, CreateServicePayload, UpdateServicePayload } from './types'
|
|
3
|
+
import type { Service, CreateServicePayload, ServiceLimits, UpdateServicePayload } from './types'
|
|
4
4
|
|
|
5
5
|
export class ServicesApi extends ArchivableCrudApi<
|
|
6
6
|
Service,
|
|
@@ -10,4 +10,13 @@ export class ServicesApi extends ArchivableCrudApi<
|
|
|
10
10
|
constructor(client: ApiClient) {
|
|
11
11
|
super(client, '/services')
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns, for each connection type of the account plan, how many connections are used,
|
|
16
|
+
* allowed and still available.
|
|
17
|
+
* @permissions services.view
|
|
18
|
+
*/
|
|
19
|
+
getLimits(headers?: Record<string, string>): Promise<ServiceLimits> {
|
|
20
|
+
return this.client.get<ServiceLimits>(`${this.basePath}/limits`, headers)
|
|
21
|
+
}
|
|
13
22
|
}
|
|
@@ -221,3 +221,12 @@ export type UpdateServicePayload = {
|
|
|
221
221
|
botId?: string | null
|
|
222
222
|
archivedAt?: string | null
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
export type ServiceLimit = {
|
|
226
|
+
used: number
|
|
227
|
+
total: number
|
|
228
|
+
available: number
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Connections used and allowed by the account plan, by connection type. */
|
|
232
|
+
export type ServiceLimits = Partial<Record<ServiceType, ServiceLimit>>
|
|
@@ -64,7 +64,7 @@ describe('StickerUsersApi', () => {
|
|
|
64
64
|
it('serializes query parameters', async () => {
|
|
65
65
|
vi.mocked(client.get).mockResolvedValue(paginated)
|
|
66
66
|
await api.getMany({ page: 2, perPage: 5 })
|
|
67
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
67
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
68
68
|
expect(url).toContain('/sticker-users?')
|
|
69
69
|
expect(decodeURIComponent(url)).toContain('"page":2')
|
|
70
70
|
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { TagsApi } from './TagsApi'
|
|
3
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
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
|
+
|
|
21
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('TagsApi', () => {
|
|
24
|
+
let client: ReturnType<typeof createMockClient>
|
|
25
|
+
let api: TagsApi
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = createMockClient()
|
|
29
|
+
api = new TagsApi(client)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('attachContacts', () => {
|
|
33
|
+
it('calls client.post with /tags/:id/contacts and the selection', async () => {
|
|
34
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
35
|
+
const body = { where: { id: { $in: ['c1', 'c2'] } } }
|
|
36
|
+
const result = await api.attachContacts('t1', body)
|
|
37
|
+
expect(client.post).toHaveBeenCalledWith('/tags/t1/contacts', body, undefined)
|
|
38
|
+
expect(result).toBe('OK')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('forwards custom headers', async () => {
|
|
42
|
+
await api.attachContacts('t1', { where: { id: 'c1' } }, headers)
|
|
43
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
44
|
+
'/tags/t1/contacts',
|
|
45
|
+
{ where: { id: 'c1' } },
|
|
46
|
+
headers,
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('detachContacts', () => {
|
|
52
|
+
it('calls client.request with DELETE /tags/:id/contacts and the selection', async () => {
|
|
53
|
+
vi.mocked(client.request).mockResolvedValue('OK')
|
|
54
|
+
const body = { where: { id: 'c1' } }
|
|
55
|
+
const result = await api.detachContacts('t1', body, headers)
|
|
56
|
+
expect(client.request).toHaveBeenCalledWith('DELETE', '/tags/t1/contacts', body, headers)
|
|
57
|
+
expect(result).toBe('OK')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('attachManyToDepartments', () => {
|
|
62
|
+
it('calls client.post with /tags/attach-many-departments and body', async () => {
|
|
63
|
+
vi.mocked(client.post).mockResolvedValue('OK')
|
|
64
|
+
const body = { ids: ['t1', 't2'], departmentIds: ['d1'] }
|
|
65
|
+
const result = await api.attachManyToDepartments(body)
|
|
66
|
+
expect(client.post).toHaveBeenCalledWith('/tags/attach-many-departments', body, undefined)
|
|
67
|
+
expect(result).toBe('OK')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('error propagation', () => {
|
|
72
|
+
it('propagates errors from attachManyToDepartments', async () => {
|
|
73
|
+
vi.mocked(client.post).mockRejectedValue(new Error('Invalid tag ids'))
|
|
74
|
+
await expect(api.attachManyToDepartments({ ids: [], departmentIds: [] })).rejects.toThrow(
|
|
75
|
+
'Invalid tag ids',
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
})
|
package/src/apis/tags/TagsApi.ts
CHANGED
|
@@ -1,9 +1,65 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { BaseCrudApi } from '../../core/BaseCrudApi'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ContactsSelection } from '../contacts/types'
|
|
4
|
+
import type {
|
|
5
|
+
AttachTagsToDepartmentsPayload,
|
|
6
|
+
CreateTagPayload,
|
|
7
|
+
Tag,
|
|
8
|
+
UpdateTagPayload,
|
|
9
|
+
} from './types'
|
|
4
10
|
|
|
5
11
|
export class TagsApi extends BaseCrudApi<Tag, CreateTagPayload, UpdateTagPayload> {
|
|
6
12
|
constructor(client: ApiClient) {
|
|
7
13
|
super(client, '/tags')
|
|
8
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adds the tag to every contact of the account that matches the selection. Resolves to `'OK'`.
|
|
18
|
+
* @permissions tags.view
|
|
19
|
+
*/
|
|
20
|
+
attachContacts(
|
|
21
|
+
id: string,
|
|
22
|
+
body: ContactsSelection,
|
|
23
|
+
headers?: Record<string, string>,
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
return this.client.post<string, ContactsSelection>(
|
|
26
|
+
`${this.basePath}/${id}/contacts`,
|
|
27
|
+
body,
|
|
28
|
+
headers,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Removes the tag from every contact of the account that matches the selection. Resolves to
|
|
34
|
+
* `'OK'`.
|
|
35
|
+
* @permissions tags.view OR tags.destroy
|
|
36
|
+
*/
|
|
37
|
+
detachContacts(
|
|
38
|
+
id: string,
|
|
39
|
+
body: ContactsSelection,
|
|
40
|
+
headers?: Record<string, string>,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
return this.client.request<string, ContactsSelection>(
|
|
43
|
+
'DELETE',
|
|
44
|
+
`${this.basePath}/${id}/contacts`,
|
|
45
|
+
body,
|
|
46
|
+
headers,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Links several tags to the given departments. Resolves to `'OK'`. A tag without departments
|
|
52
|
+
* is already available to all of them and is left unchanged.
|
|
53
|
+
* @permissions tags.view
|
|
54
|
+
*/
|
|
55
|
+
attachManyToDepartments(
|
|
56
|
+
body: AttachTagsToDepartmentsPayload,
|
|
57
|
+
headers?: Record<string, string>,
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
return this.client.post<string, AttachTagsToDepartmentsPayload>(
|
|
60
|
+
`${this.basePath}/attach-many-departments`,
|
|
61
|
+
body,
|
|
62
|
+
headers,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
9
65
|
}
|
package/src/apis/tags/types.ts
CHANGED
|
@@ -27,3 +27,12 @@ export type UpdateTagPayload = {
|
|
|
27
27
|
label?: string
|
|
28
28
|
backgroundColor?: string | null
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Body for `POST /tags/attach-many-departments`. An empty `departmentIds` makes the tags
|
|
33
|
+
* available to every department.
|
|
34
|
+
*/
|
|
35
|
+
export type AttachTagsToDepartmentsPayload = {
|
|
36
|
+
ids: string[]
|
|
37
|
+
departmentIds: string[]
|
|
38
|
+
}
|
|
@@ -66,7 +66,7 @@ describe('TermsApi', () => {
|
|
|
66
66
|
it('serializes the query', async () => {
|
|
67
67
|
vi.mocked(client.get).mockResolvedValue(term)
|
|
68
68
|
await api.getById('term-1', { attributes: ['id', 'version'] })
|
|
69
|
-
const url = vi.mocked(client.get).mock.calls[0][0] as string
|
|
69
|
+
const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
|
|
70
70
|
expect(url).toContain('/terms/term-1?')
|
|
71
71
|
expect(url).toContain('attributes')
|
|
72
72
|
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { UsersApi } from './UsersApi'
|
|
3
|
+
import type { ApiClient } from '../../core/ApiClient'
|
|
4
|
+
import type {
|
|
5
|
+
BulkUsersStatusPreview,
|
|
6
|
+
BulkUsersStatusResult,
|
|
7
|
+
ImportUsersResult,
|
|
8
|
+
User,
|
|
9
|
+
UserImportFile,
|
|
10
|
+
ValidateUserImportResult,
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
// ── Mock client factory ──────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function createMockClient(): ApiClient {
|
|
16
|
+
return {
|
|
17
|
+
setAccessToken: vi.fn().mockReturnThis(),
|
|
18
|
+
request: vi.fn(),
|
|
19
|
+
get: vi.fn(),
|
|
20
|
+
post: vi.fn(),
|
|
21
|
+
put: vi.fn(),
|
|
22
|
+
patch: vi.fn(),
|
|
23
|
+
delete: vi.fn(),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Fixtures ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const user: User = {
|
|
30
|
+
id: 'usr-1',
|
|
31
|
+
name: 'Alice',
|
|
32
|
+
email: 'alice@example.com',
|
|
33
|
+
phoneNumber: null,
|
|
34
|
+
branch: null,
|
|
35
|
+
isSuperAdmin: false,
|
|
36
|
+
isClientUser: false,
|
|
37
|
+
active: true,
|
|
38
|
+
language: 'pt-BR',
|
|
39
|
+
isFirstLogin: false,
|
|
40
|
+
status: 'offline',
|
|
41
|
+
clientsStatus: { web: 'offline', app: 'offline' },
|
|
42
|
+
offlineAt: null,
|
|
43
|
+
isActiveInternalChat: false,
|
|
44
|
+
passwordExpiresAt: null,
|
|
45
|
+
otpAuthActive: false,
|
|
46
|
+
preferences: {},
|
|
47
|
+
archivedAt: null,
|
|
48
|
+
accountId: 'acc-1',
|
|
49
|
+
timetableId: null,
|
|
50
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
51
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
52
|
+
deletedAt: null,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const file: UserImportFile = { id: 'f1', name: 'users.csv', mimetype: 'text/csv' }
|
|
56
|
+
const headers = { 'X-Custom': 'value' }
|
|
57
|
+
|
|
58
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe('UsersApi', () => {
|
|
61
|
+
let client: ReturnType<typeof createMockClient>
|
|
62
|
+
let api: UsersApi
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
client = createMockClient()
|
|
66
|
+
api = new UsersApi(client)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('archive (ArchivableCrudApi)', () => {
|
|
70
|
+
it('keeps the plain PUT { archivedAt } used by the cleanup of the test suites', async () => {
|
|
71
|
+
await api.archive('usr-1')
|
|
72
|
+
const [url, body] = vi.mocked(client.put).mock.calls[0] ?? []
|
|
73
|
+
expect(url).toBe('/users/usr-1')
|
|
74
|
+
expect(body).toEqual({ archivedAt: expect.any(String) })
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('archiveWithValidation', () => {
|
|
79
|
+
it('calls client.post with /users/:id/archive and { archive: true }', async () => {
|
|
80
|
+
const archived = { ...user, archivedAt: '2026-01-01T00:00:00Z' }
|
|
81
|
+
vi.mocked(client.post).mockResolvedValue(archived)
|
|
82
|
+
const result = await api.archiveWithValidation('usr-1', { archive: true })
|
|
83
|
+
expect(client.post).toHaveBeenCalledWith('/users/usr-1/archive', { archive: true }, undefined)
|
|
84
|
+
expect(result).toEqual(archived)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('restores with { archive: false } and forwards custom headers', async () => {
|
|
88
|
+
await api.archiveWithValidation('usr-1', { archive: false }, headers)
|
|
89
|
+
expect(client.post).toHaveBeenCalledWith('/users/usr-1/archive', { archive: false }, headers)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('previewBulkStatus', () => {
|
|
94
|
+
it('calls client.post with /users/bulk-status/preview and body', async () => {
|
|
95
|
+
const preview: BulkUsersStatusPreview = {
|
|
96
|
+
scenario: 'allEligible',
|
|
97
|
+
totalSelected: 1,
|
|
98
|
+
noop: 0,
|
|
99
|
+
eligible: 1,
|
|
100
|
+
blocked: 0,
|
|
101
|
+
blockedSelf: false,
|
|
102
|
+
planSlotsAvailable: 10,
|
|
103
|
+
}
|
|
104
|
+
vi.mocked(client.post).mockResolvedValue(preview)
|
|
105
|
+
const body = { ids: ['usr-1'], archive: false }
|
|
106
|
+
const result = await api.previewBulkStatus(body)
|
|
107
|
+
expect(client.post).toHaveBeenCalledWith('/users/bulk-status/preview', body, undefined)
|
|
108
|
+
expect(result).toEqual(preview)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('bulkStatus', () => {
|
|
113
|
+
it('calls client.post with /users/bulk-status and body', async () => {
|
|
114
|
+
const response: BulkUsersStatusResult = {
|
|
115
|
+
changed: 1,
|
|
116
|
+
noop: 0,
|
|
117
|
+
blocked: 0,
|
|
118
|
+
failed: 0,
|
|
119
|
+
changedIds: ['usr-1'],
|
|
120
|
+
missingIds: [],
|
|
121
|
+
}
|
|
122
|
+
vi.mocked(client.post).mockResolvedValue(response)
|
|
123
|
+
const body = { ids: ['usr-1'], archive: true }
|
|
124
|
+
const result = await api.bulkStatus(body, headers)
|
|
125
|
+
expect(client.post).toHaveBeenCalledWith('/users/bulk-status', body, headers)
|
|
126
|
+
expect(result).toEqual(response)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('exportCsv', () => {
|
|
131
|
+
it('calls client.post with /users/export and resolves to the CSV text', async () => {
|
|
132
|
+
const csv = 'Nome,E-mail\nAlice,alice@example.com\n'
|
|
133
|
+
vi.mocked(client.post).mockResolvedValue(csv)
|
|
134
|
+
const result = await api.exportCsv({ filter: 'active' })
|
|
135
|
+
expect(client.post).toHaveBeenCalledWith('/users/export', { filter: 'active' }, undefined)
|
|
136
|
+
expect(result).toBe(csv)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('validateImport', () => {
|
|
141
|
+
it('calls client.post with /users/import/validate and the file', async () => {
|
|
142
|
+
const response: ValidateUserImportResult = {
|
|
143
|
+
licenses: { requestedRows: 1, available: 5, exceeded: false },
|
|
144
|
+
columns: {
|
|
145
|
+
required: [{ name: 'Nome', found: true }],
|
|
146
|
+
missingRequired: [],
|
|
147
|
+
optionalFound: [],
|
|
148
|
+
},
|
|
149
|
+
rows: { total: 1, valid: 1, invalid: 0, hasInvalid: false },
|
|
150
|
+
rowsWithOptionalColumns: { total: 1, valid: 1, invalid: 0, hasInvalid: false },
|
|
151
|
+
}
|
|
152
|
+
vi.mocked(client.post).mockResolvedValue(response)
|
|
153
|
+
const result = await api.validateImport({ file })
|
|
154
|
+
expect(client.post).toHaveBeenCalledWith('/users/import/validate', { file }, undefined)
|
|
155
|
+
expect(result).toEqual(response)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('startImport', () => {
|
|
160
|
+
it('calls client.post with /users/import and body', async () => {
|
|
161
|
+
const response: ImportUsersResult = { importId: 'imp-1' }
|
|
162
|
+
vi.mocked(client.post).mockResolvedValue(response)
|
|
163
|
+
const body = { file, useOptionalColumns: true }
|
|
164
|
+
const result = await api.startImport(body, headers)
|
|
165
|
+
expect(client.post).toHaveBeenCalledWith('/users/import', body, headers)
|
|
166
|
+
expect(result).toEqual(response)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('disableOtpAuth', () => {
|
|
171
|
+
it('calls client.post with /users/:id/disable-otp-auth and no body', async () => {
|
|
172
|
+
vi.mocked(client.post).mockResolvedValue(user)
|
|
173
|
+
const result = await api.disableOtpAuth('usr-1')
|
|
174
|
+
expect(client.post).toHaveBeenCalledWith(
|
|
175
|
+
'/users/usr-1/disable-otp-auth',
|
|
176
|
+
undefined,
|
|
177
|
+
undefined,
|
|
178
|
+
)
|
|
179
|
+
expect(result).toEqual(user)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('enableAccess', () => {
|
|
184
|
+
it('calls client.post with /users/:id/enable-access and no body', async () => {
|
|
185
|
+
const response = { success: true, message: 'User access enabled successfully' }
|
|
186
|
+
vi.mocked(client.post).mockResolvedValue(response)
|
|
187
|
+
const result = await api.enableAccess('usr-1', headers)
|
|
188
|
+
expect(client.post).toHaveBeenCalledWith('/users/usr-1/enable-access', undefined, headers)
|
|
189
|
+
expect(result).toEqual(response)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('getPinnedContacts', () => {
|
|
194
|
+
it('calls client.get with /users/pin', async () => {
|
|
195
|
+
vi.mocked(client.get).mockResolvedValue([])
|
|
196
|
+
const result = await api.getPinnedContacts()
|
|
197
|
+
expect(client.get).toHaveBeenCalledWith('/users/pin', undefined)
|
|
198
|
+
expect(result).toEqual([])
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('togglePinContact', () => {
|
|
203
|
+
it('calls client.patch with /users/pin and resolves to the link rows when pinning', async () => {
|
|
204
|
+
const links = [
|
|
205
|
+
{ userId: 'usr-1', contactId: 'c1', createdAt: '2026-01-01', updatedAt: '2026-01-01' },
|
|
206
|
+
]
|
|
207
|
+
vi.mocked(client.patch).mockResolvedValue(links)
|
|
208
|
+
const result = await api.togglePinContact({ contactId: 'c1' })
|
|
209
|
+
expect(client.patch).toHaveBeenCalledWith('/users/pin', { contactId: 'c1' }, undefined)
|
|
210
|
+
expect(result).toEqual(links)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('resolves to the number of removed links when unpinning', async () => {
|
|
214
|
+
vi.mocked(client.patch).mockResolvedValue(1)
|
|
215
|
+
await expect(api.togglePinContact({ contactId: 'c1' }, headers)).resolves.toBe(1)
|
|
216
|
+
expect(client.patch).toHaveBeenCalledWith('/users/pin', { contactId: 'c1' }, headers)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('error propagation', () => {
|
|
221
|
+
it('propagates errors from archiveWithValidation', async () => {
|
|
222
|
+
vi.mocked(client.post).mockRejectedValue(new Error('Bad request'))
|
|
223
|
+
await expect(api.archiveWithValidation('usr-1', { archive: true })).rejects.toThrow(
|
|
224
|
+
'Bad request',
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -1,9 +1,160 @@
|
|
|
1
1
|
import type { ApiClient } from '../../core/ApiClient'
|
|
2
2
|
import { ArchivableCrudApi } from '../../core/ArchivableCrudApi'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Contact } from '../contacts/types'
|
|
4
|
+
import type {
|
|
5
|
+
ArchiveUserPayload,
|
|
6
|
+
BulkUsersStatusPayload,
|
|
7
|
+
BulkUsersStatusPreview,
|
|
8
|
+
BulkUsersStatusResult,
|
|
9
|
+
CreateUserPayload,
|
|
10
|
+
EnableUserAccessResult,
|
|
11
|
+
ExportUsersPayload,
|
|
12
|
+
ImportUsersPayload,
|
|
13
|
+
ImportUsersResult,
|
|
14
|
+
TogglePinContactPayload,
|
|
15
|
+
TogglePinContactResult,
|
|
16
|
+
UpdateUserPayload,
|
|
17
|
+
User,
|
|
18
|
+
ValidateUserImportPayload,
|
|
19
|
+
ValidateUserImportResult,
|
|
20
|
+
} from './types'
|
|
4
21
|
|
|
5
22
|
export class UsersApi extends ArchivableCrudApi<User, CreateUserPayload, UpdateUserPayload> {
|
|
6
23
|
constructor(client: ApiClient) {
|
|
7
24
|
super(client, '/users')
|
|
8
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Archives or restores a user through the dedicated route used by the UI, which also
|
|
29
|
+
* sets the user offline and ends its sessions. The route enforces the business rules that
|
|
30
|
+
* {@link archive}/{@link unarchive} (a plain `PUT { archivedAt }`) skip:
|
|
31
|
+
* - archiving refuses (403) a user referenced by a bot or with open tickets;
|
|
32
|
+
* - restoring refuses (402) when the plan has no free user slot.
|
|
33
|
+
* @permissions users.view OR users.archive
|
|
34
|
+
*/
|
|
35
|
+
archiveWithValidation(
|
|
36
|
+
id: string,
|
|
37
|
+
body: ArchiveUserPayload,
|
|
38
|
+
headers?: Record<string, string>,
|
|
39
|
+
): Promise<User> {
|
|
40
|
+
return this.client.post<User, ArchiveUserPayload>(
|
|
41
|
+
`${this.basePath}/${id}/archive`,
|
|
42
|
+
body,
|
|
43
|
+
headers,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Previews a bulk archive/unarchive without changing anything.
|
|
49
|
+
* @permissions users.archive
|
|
50
|
+
*/
|
|
51
|
+
previewBulkStatus(
|
|
52
|
+
body: BulkUsersStatusPayload,
|
|
53
|
+
headers?: Record<string, string>,
|
|
54
|
+
): Promise<BulkUsersStatusPreview> {
|
|
55
|
+
return this.client.post<BulkUsersStatusPreview, BulkUsersStatusPayload>(
|
|
56
|
+
`${this.basePath}/bulk-status/preview`,
|
|
57
|
+
body,
|
|
58
|
+
headers,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Archives or unarchives up to 100 users at once.
|
|
64
|
+
* @permissions users.archive
|
|
65
|
+
*/
|
|
66
|
+
bulkStatus(
|
|
67
|
+
body: BulkUsersStatusPayload,
|
|
68
|
+
headers?: Record<string, string>,
|
|
69
|
+
): Promise<BulkUsersStatusResult> {
|
|
70
|
+
return this.client.post<BulkUsersStatusResult, BulkUsersStatusPayload>(
|
|
71
|
+
`${this.basePath}/bulk-status`,
|
|
72
|
+
body,
|
|
73
|
+
headers,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Exports the users of the account as CSV. Resolves to the CSV text.
|
|
79
|
+
* @permissions users.export
|
|
80
|
+
*/
|
|
81
|
+
exportCsv(body: ExportUsersPayload, headers?: Record<string, string>): Promise<string> {
|
|
82
|
+
return this.client.post<string, ExportUsersPayload>(`${this.basePath}/export`, body, headers)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validates a CSV file previously uploaded through `FilesApi` before importing it.
|
|
87
|
+
* @permissions users.create
|
|
88
|
+
*/
|
|
89
|
+
validateImport(
|
|
90
|
+
body: ValidateUserImportPayload,
|
|
91
|
+
headers?: Record<string, string>,
|
|
92
|
+
): Promise<ValidateUserImportResult> {
|
|
93
|
+
return this.client.post<ValidateUserImportResult, ValidateUserImportPayload>(
|
|
94
|
+
`${this.basePath}/import/validate`,
|
|
95
|
+
body,
|
|
96
|
+
headers,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Starts the import of a CSV file previously uploaded through `FilesApi`. The import runs
|
|
102
|
+
* in the background (the route answers `202 Accepted`).
|
|
103
|
+
* @permissions users.create
|
|
104
|
+
*/
|
|
105
|
+
startImport(
|
|
106
|
+
body: ImportUsersPayload,
|
|
107
|
+
headers?: Record<string, string>,
|
|
108
|
+
): Promise<ImportUsersResult> {
|
|
109
|
+
return this.client.post<ImportUsersResult, ImportUsersPayload>(
|
|
110
|
+
`${this.basePath}/import`,
|
|
111
|
+
body,
|
|
112
|
+
headers,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Turns off the two-factor authentication (OTP) of a user.
|
|
118
|
+
* @permissions users.update AND myAccount.update
|
|
119
|
+
*/
|
|
120
|
+
disableOtpAuth(id: string, headers?: Record<string, string>): Promise<User> {
|
|
121
|
+
return this.client.post<User>(`${this.basePath}/${id}/disable-otp-auth`, undefined, headers)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Clears the login throttling of a user blocked by too many failed attempts.
|
|
126
|
+
* @permissions users.update
|
|
127
|
+
*/
|
|
128
|
+
enableAccess(id: string, headers?: Record<string, string>): Promise<EnableUserAccessResult> {
|
|
129
|
+
return this.client.post<EnableUserAccessResult>(
|
|
130
|
+
`${this.basePath}/${id}/enable-access`,
|
|
131
|
+
undefined,
|
|
132
|
+
headers,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns the contacts pinned by the logged user.
|
|
138
|
+
* @permissions contacts.pin
|
|
139
|
+
*/
|
|
140
|
+
getPinnedContacts(headers?: Record<string, string>): Promise<Contact[]> {
|
|
141
|
+
return this.client.get<Contact[]>(`${this.basePath}/pin`, headers)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Pins the contact for the logged user, or unpins it when already pinned. A user can pin
|
|
146
|
+
* up to 3 contacts. Resolves to the created link rows when pinning, or to the number of
|
|
147
|
+
* removed links when unpinning.
|
|
148
|
+
* @permissions contacts.pin
|
|
149
|
+
*/
|
|
150
|
+
togglePinContact(
|
|
151
|
+
body: TogglePinContactPayload,
|
|
152
|
+
headers?: Record<string, string>,
|
|
153
|
+
): Promise<TogglePinContactResult> {
|
|
154
|
+
return this.client.patch<TogglePinContactResult, TogglePinContactPayload>(
|
|
155
|
+
`${this.basePath}/pin`,
|
|
156
|
+
body,
|
|
157
|
+
headers,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
9
160
|
}
|