@ikatec/digisac-api-sdk 4.10.0 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/apis/categories/types.d.ts +1 -0
  2. package/dist/apis/categories/types.d.ts.map +1 -1
  3. package/dist/apis/contactBlockLists/ContactBlockListsApi.cjs +28 -0
  4. package/dist/apis/contactBlockLists/ContactBlockListsApi.cjs.map +1 -1
  5. package/dist/apis/contactBlockLists/ContactBlockListsApi.d.ts +24 -0
  6. package/dist/apis/contactBlockLists/ContactBlockListsApi.d.ts.map +1 -1
  7. package/dist/apis/contactBlockLists/ContactBlockListsApi.mjs +28 -0
  8. package/dist/apis/contactBlockLists/ContactBlockListsApi.mjs.map +1 -1
  9. package/dist/apis/contactBlockLists/types.d.ts +6 -1
  10. package/dist/apis/contactBlockLists/types.d.ts.map +1 -1
  11. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.cjs +31 -1
  12. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.cjs.map +1 -1
  13. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.d.ts +28 -2
  14. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.d.ts.map +1 -1
  15. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.mjs +31 -1
  16. package/dist/apis/contactBlockListsControls/ContactBlockListsControlsApi.mjs.map +1 -1
  17. package/dist/apis/contactBlockListsControls/types.d.ts +7 -0
  18. package/dist/apis/contactBlockListsControls/types.d.ts.map +1 -1
  19. package/dist/apis/distribution/DistributionApi.cjs +25 -0
  20. package/dist/apis/distribution/DistributionApi.cjs.map +1 -1
  21. package/dist/apis/distribution/DistributionApi.d.ts +20 -1
  22. package/dist/apis/distribution/DistributionApi.d.ts.map +1 -1
  23. package/dist/apis/distribution/DistributionApi.mjs +25 -0
  24. package/dist/apis/distribution/DistributionApi.mjs.map +1 -1
  25. package/dist/apis/distribution/types.d.ts +18 -1
  26. package/dist/apis/distribution/types.d.ts.map +1 -1
  27. package/dist/apis/integrations/IntegrationsApi.cjs +17 -0
  28. package/dist/apis/integrations/IntegrationsApi.cjs.map +1 -1
  29. package/dist/apis/integrations/IntegrationsApi.d.ts +15 -2
  30. package/dist/apis/integrations/IntegrationsApi.d.ts.map +1 -1
  31. package/dist/apis/integrations/IntegrationsApi.mjs +17 -0
  32. package/dist/apis/integrations/IntegrationsApi.mjs.map +1 -1
  33. package/dist/apis/integrations/types.d.ts +15 -6
  34. package/dist/apis/integrations/types.d.ts.map +1 -1
  35. package/dist/apis/interactiveMessages/InteractiveMessagesApi.cjs +15 -0
  36. package/dist/apis/interactiveMessages/InteractiveMessagesApi.cjs.map +1 -1
  37. package/dist/apis/interactiveMessages/InteractiveMessagesApi.d.ts +13 -0
  38. package/dist/apis/interactiveMessages/InteractiveMessagesApi.d.ts.map +1 -1
  39. package/dist/apis/interactiveMessages/InteractiveMessagesApi.mjs +15 -0
  40. package/dist/apis/interactiveMessages/InteractiveMessagesApi.mjs.map +1 -1
  41. package/dist/apis/interactiveMessages/types.d.ts +8 -2
  42. package/dist/apis/interactiveMessages/types.d.ts.map +1 -1
  43. package/dist/apis/me/MeApi.cjs +36 -29
  44. package/dist/apis/me/MeApi.cjs.map +1 -1
  45. package/dist/apis/me/MeApi.d.ts +33 -22
  46. package/dist/apis/me/MeApi.d.ts.map +1 -1
  47. package/dist/apis/me/MeApi.mjs +36 -29
  48. package/dist/apis/me/MeApi.mjs.map +1 -1
  49. package/dist/apis/me/types.d.ts +67 -4
  50. package/dist/apis/me/types.d.ts.map +1 -1
  51. package/dist/apis/transcripts/TranscriptsApi.cjs +12 -2
  52. package/dist/apis/transcripts/TranscriptsApi.cjs.map +1 -1
  53. package/dist/apis/transcripts/TranscriptsApi.d.ts +11 -5
  54. package/dist/apis/transcripts/TranscriptsApi.d.ts.map +1 -1
  55. package/dist/apis/transcripts/TranscriptsApi.mjs +12 -2
  56. package/dist/apis/transcripts/TranscriptsApi.mjs.map +1 -1
  57. package/dist/apis/transcripts/types.d.ts +4 -0
  58. package/dist/apis/transcripts/types.d.ts.map +1 -1
  59. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.cjs +52 -0
  60. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.cjs.map +1 -1
  61. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.d.ts +45 -2
  62. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.d.ts.map +1 -1
  63. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.mjs +52 -0
  64. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.mjs.map +1 -1
  65. package/dist/apis/whatsappBusinessTemplates/types.d.ts +34 -1
  66. package/dist/apis/whatsappBusinessTemplates/types.d.ts.map +1 -1
  67. package/dist/core/BaseCrudApi.cjs +3 -1
  68. package/dist/core/BaseCrudApi.cjs.map +1 -1
  69. package/dist/core/BaseCrudApi.d.ts +6 -4
  70. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  71. package/dist/core/BaseCrudApi.mjs +3 -1
  72. package/dist/core/BaseCrudApi.mjs.map +1 -1
  73. package/dist/core/types.d.ts +1 -1
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/apis/categories/types.ts +1 -0
  77. package/src/apis/contactBlockLists/ContactBlockListsApi.test.ts +57 -0
  78. package/src/apis/contactBlockLists/ContactBlockListsApi.ts +44 -0
  79. package/src/apis/contactBlockLists/types.ts +6 -1
  80. package/src/apis/contactBlockListsControls/ContactBlockListsControlsApi.test.ts +48 -0
  81. package/src/apis/contactBlockListsControls/ContactBlockListsControlsApi.ts +49 -2
  82. package/src/apis/contactBlockListsControls/types.ts +7 -0
  83. package/src/apis/distribution/DistributionApi.test.ts +65 -0
  84. package/src/apis/distribution/DistributionApi.ts +39 -1
  85. package/src/apis/distribution/types.ts +17 -1
  86. package/src/apis/integrations/IntegrationsApi.test.ts +42 -0
  87. package/src/apis/integrations/IntegrationsApi.ts +35 -2
  88. package/src/apis/integrations/types.ts +25 -6
  89. package/src/apis/interactiveMessages/InteractiveMessagesApi.test.ts +59 -0
  90. package/src/apis/interactiveMessages/InteractiveMessagesApi.ts +26 -0
  91. package/src/apis/interactiveMessages/types.ts +8 -2
  92. package/src/apis/me/MeApi.test.ts +44 -0
  93. package/src/apis/me/MeApi.ts +83 -28
  94. package/src/apis/me/types.ts +75 -4
  95. package/src/apis/payloads.test-d.ts +94 -2
  96. package/src/apis/transcripts/TranscriptsApi.test.ts +14 -21
  97. package/src/apis/transcripts/TranscriptsApi.ts +15 -6
  98. package/src/apis/transcripts/types.ts +4 -0
  99. package/src/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.test.ts +86 -0
  100. package/src/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.ts +75 -1
  101. package/src/apis/whatsappBusinessTemplates/types.ts +38 -1
  102. package/src/core/BaseCrudApi.test-d.ts +11 -1
  103. package/src/core/BaseCrudApi.ts +15 -6
  104. package/src/core/types.ts +4 -2
@@ -11,7 +11,8 @@ export type Distribution = {
11
11
  distributeQueue: boolean
12
12
  accountId: string
13
13
  archivedAt: string | null
14
- deletedAt: string | null
14
+ /** Only in the answer of `updateById` (the stored model, not transformed). */
15
+ deletedAt?: string | null
15
16
  createdAt: string
16
17
  updatedAt: string
17
18
  // relationships
@@ -21,6 +22,9 @@ export type Distribution = {
21
22
 
22
23
  export type DistributionRelationships = 'departments' | 'roles'
23
24
 
25
+ /** A department or a role linked to a distribution (only the id is read). */
26
+ export type DistributionLink = { id: string }
27
+
24
28
  export type CreateDistributionPayload = {
25
29
  name: string
26
30
  maxNum: number
@@ -28,8 +32,17 @@ export type CreateDistributionPayload = {
28
32
  timeToRedistribute?: number
29
33
  redistributeAssignedTickets?: boolean
30
34
  distributeQueue?: boolean
35
+ /** An archived distribution is skipped by the distribution crons. */
36
+ archivedAt?: string | null
37
+ /** Departments to distribute; a department has at most one distribution. */
38
+ departments?: DistributionLink[]
39
+ roles?: DistributionLink[]
31
40
  }
32
41
 
42
+ /**
43
+ * The update replaces the links: send `departments` and `roles` again to keep them — omitting them
44
+ * clears them.
45
+ */
33
46
  export type UpdateDistributionPayload = {
34
47
  name?: string
35
48
  maxNum?: number
@@ -37,4 +50,7 @@ export type UpdateDistributionPayload = {
37
50
  timeToRedistribute?: number
38
51
  redistributeAssignedTickets?: boolean
39
52
  distributeQueue?: boolean
53
+ archivedAt?: string | null
54
+ departments?: DistributionLink[]
55
+ roles?: DistributionLink[]
40
56
  }
@@ -0,0 +1,42 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { IntegrationsApi } from './IntegrationsApi'
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
+ // ── Tests ────────────────────────────────────────────────────────────
20
+
21
+ describe('IntegrationsApi', () => {
22
+ let client: ReturnType<typeof createMockClient>
23
+ let api: IntegrationsApi
24
+
25
+ beforeEach(() => {
26
+ client = createMockClient()
27
+ api = new IntegrationsApi(client)
28
+ })
29
+
30
+ it('create posts to /integrations and resolves the empty object the route answers', async () => {
31
+ vi.mocked(client.post).mockResolvedValue({})
32
+ const body = { type: 'menu_button', url: 'https://example.com', icon: '', text: 'QA' }
33
+ await expect(api.create(body)).resolves.toEqual({})
34
+ expect(client.post).toHaveBeenCalledWith('/integrations', body, undefined)
35
+ })
36
+
37
+ it('updateById puts to /integrations/:id and resolves the empty object', async () => {
38
+ vi.mocked(client.put).mockResolvedValue({})
39
+ await expect(api.updateById('int-1', { text: 'QA 2' })).resolves.toEqual({})
40
+ expect(client.put).toHaveBeenCalledWith('/integrations/int-1', { text: 'QA 2' }, undefined)
41
+ })
42
+ })
@@ -1,19 +1,52 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import type { ListQuery } from '../../core/types'
3
3
  import { BaseCrudApi } from '../../core/BaseCrudApi'
4
- import type { Integration, CreateIntegrationPayload, UpdateIntegrationPayload } from './types'
4
+ import type {
5
+ Integration,
6
+ CreateIntegrationPayload,
7
+ UpdateIntegrationPayload,
8
+ IntegrationWriteResponse,
9
+ } from './types'
5
10
 
11
+ /** Integrations of the account (buttons and events of the UI). `getById` answers `null` for an unknown id. */
6
12
  export class IntegrationsApi extends BaseCrudApi<
7
13
  Integration,
8
14
  CreateIntegrationPayload,
9
15
  UpdateIntegrationPayload,
10
16
  ListQuery<Integration>,
11
- 'OK'
17
+ 'OK',
18
+ IntegrationWriteResponse,
19
+ IntegrationWriteResponse
12
20
  > {
13
21
  constructor(client: ApiClient) {
14
22
  super(client, '/integrations')
15
23
  }
16
24
 
25
+ /**
26
+ * Creates an integration. Resolves `{}` — also when the type is unique (`event_ticket_transfer`,
27
+ * `event_ticket_closed`), already exists in the account and nothing is created. Find the integration with
28
+ * `getMany` (e.g. by `text`).
29
+ * @permissions integrations.create
30
+ */
31
+ override create(
32
+ body: CreateIntegrationPayload,
33
+ headers?: Record<string, string>,
34
+ ): Promise<IntegrationWriteResponse> {
35
+ return super.create(body, headers)
36
+ }
37
+
38
+ /**
39
+ * Updates an integration. Resolves `{}`; read it again with `getById`. An unknown id answers 500.
40
+ * @permissions integrations.view OR integrations.update
41
+ */
42
+ override updateById(
43
+ id: string,
44
+ body: UpdateIntegrationPayload,
45
+ headers?: Record<string, string>,
46
+ ): Promise<IntegrationWriteResponse> {
47
+ return super.updateById(id, body, headers)
48
+ }
49
+
17
50
  /**
18
51
  * Deletes an integration. Resolves the text `'OK'`; an unknown id answers 500.
19
52
  * @permissions integrations.view OR integrations.destroy
@@ -1,27 +1,46 @@
1
+ /**
2
+ * The types the UI shows: buttons (`menu_button` in the header, `context_menu_button_*` in the contact and
3
+ * message menus) and events that open the integration when a ticket is transferred or closed (unique per
4
+ * account). The backend accepts any string.
5
+ */
6
+ export type IntegrationType =
7
+ | 'menu_button'
8
+ | 'context_menu_button_contact_modal'
9
+ | 'context_menu_button_contact_side_list'
10
+ | 'context_menu_button_message'
11
+ | 'event_ticket_transfer'
12
+ | 'event_ticket_closed'
13
+ | (string & {})
14
+
15
+ export type IntegrationSize = 'sm' | 'md' | 'lg' | (string & {})
16
+
1
17
  export type Integration = {
2
18
  id: string
3
- type: string
19
+ type: IntegrationType
4
20
  url: string | null
5
21
  icon: string | null
6
22
  text: string | null
7
- size: string | null
23
+ size: IntegrationSize | null
8
24
  accountId: string
9
25
  createdAt: string
10
26
  updatedAt: string
11
27
  }
12
28
 
29
+ /** What `create` and `updateById` resolve to: an empty object. */
30
+ export type IntegrationWriteResponse = Record<string, never>
31
+
13
32
  export type CreateIntegrationPayload = {
14
- type: string
33
+ type: IntegrationType
15
34
  url?: string
16
35
  icon?: string
17
36
  text?: string
18
- size?: string
37
+ size?: IntegrationSize
19
38
  }
20
39
 
21
40
  export type UpdateIntegrationPayload = {
22
- type?: string
41
+ type?: IntegrationType
23
42
  url?: string | null
24
43
  icon?: string | null
25
44
  text?: string | null
26
- size?: string | null
45
+ size?: IntegrationSize | null
27
46
  }
@@ -0,0 +1,59 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { InteractiveMessagesApi } from './InteractiveMessagesApi'
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
+ // ── Tests ────────────────────────────────────────────────────────────
20
+
21
+ describe('InteractiveMessagesApi', () => {
22
+ let client: ReturnType<typeof createMockClient>
23
+ let api: InteractiveMessagesApi
24
+
25
+ beforeEach(() => {
26
+ client = createMockClient()
27
+ api = new InteractiveMessagesApi(client)
28
+ })
29
+
30
+ describe('count', () => {
31
+ it('calls client.get with /interactive-messages/count when no query is provided', async () => {
32
+ vi.mocked(client.get).mockResolvedValue({ count: 4 })
33
+ await expect(api.count()).resolves.toEqual({ count: 4 })
34
+ expect(client.get).toHaveBeenCalledWith('/interactive-messages/count', undefined)
35
+ })
36
+
37
+ it('serializes the query', async () => {
38
+ vi.mocked(client.get).mockResolvedValue({ count: 1 })
39
+ await api.count({ where: { name: 'QA' } })
40
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
41
+ expect(url).toContain('/interactive-messages/count?')
42
+ expect(url).toContain('where')
43
+ })
44
+ })
45
+
46
+ it('create posts the message to /interactive-messages', async () => {
47
+ const body = {
48
+ name: 'QA',
49
+ interactive: {
50
+ type: 'button' as const,
51
+ body: 'Oi',
52
+ action: { buttons: [{ type: 'reply' as const, reply: { id: '1', title: 'Sim' } }] },
53
+ },
54
+ }
55
+ vi.mocked(client.post).mockResolvedValue({ id: 'im-1' })
56
+ await api.create(body)
57
+ expect(client.post).toHaveBeenCalledWith('/interactive-messages', body, undefined)
58
+ })
59
+ })
@@ -18,6 +18,32 @@ export class InteractiveMessagesApi extends BaseCrudApi<
18
18
  super(client, '/interactive-messages')
19
19
  }
20
20
 
21
+ /**
22
+ * Counts the interactive messages matching the query.
23
+ * @permissions interactive-messages.view OR interactive-messages.management.all.departments
24
+ */
25
+ count(
26
+ query?: ListQuery<InteractiveMessage>,
27
+ headers?: Record<string, string>,
28
+ ): Promise<{ count: number }> {
29
+ return this.client.get<{ count: number }>(
30
+ `${this.basePath}/count${this.toQueryString(query)}`,
31
+ headers,
32
+ )
33
+ }
34
+
35
+ /**
36
+ * Creates an interactive message. Only `cta_url` is validated by the backend (the other types are stored as
37
+ * sent). The answer has no `departments` nor `file`: read them with `getById` and `include`.
38
+ * @permissions interactive-messages.create OR interactive-messages.management.all.departments
39
+ */
40
+ override create(
41
+ body: CreateInteractiveMessagePayload,
42
+ headers?: Record<string, string>,
43
+ ): Promise<InteractiveMessage> {
44
+ return super.create(body, headers)
45
+ }
46
+
21
47
  /**
22
48
  * Deletes an interactive message. Resolves the text `'OK'`; an unknown id answers 500.
23
49
  * @permissions interactive-messages.destroy OR interactive-messages.management.all.departments
@@ -169,7 +169,8 @@ type InteractiveMessageFilePayload =
169
169
  export type InteractiveMessage = {
170
170
  id: string
171
171
  name: string
172
- type: 'list' | 'button' | 'cta_url' | 'request_contact_info'
172
+ /** @deprecated Not a field of the backend (no such column): the type is `interactive.type`. */
173
+ type?: 'list' | 'button' | 'cta_url' | 'request_contact_info'
173
174
  interactive: InteractiveMessageContent
174
175
  archivedAt: string | null
175
176
  accountId: string
@@ -179,9 +180,10 @@ export type InteractiveMessage = {
179
180
  // relationships
180
181
  file?: File | null
181
182
  departments?: Department[]
183
+ account?: Record<string, unknown>
182
184
  }
183
185
 
184
- export type InteractiveMessageRelationships = 'file' | 'departments'
186
+ export type InteractiveMessageRelationships = 'file' | 'departments' | 'account'
185
187
 
186
188
  export type CreateInteractiveMessagePayload = {
187
189
  name: string
@@ -191,6 +193,10 @@ export type CreateInteractiveMessagePayload = {
191
193
  file?: InteractiveMessageFilePayload
192
194
  }
193
195
 
196
+ /**
197
+ * The backend requires `name` and `interactive` on the update too. It replaces the departments: omitting
198
+ * `departmentIds`/`departments` clears them. Only `file.base64Url` is read (a `file.id` is ignored).
199
+ */
194
200
  export type UpdateInteractiveMessagePayload = {
195
201
  name?: string
196
202
  interactive?: InteractiveMessageContentInput
@@ -110,6 +110,50 @@ describe('MeApi', () => {
110
110
  })
111
111
  })
112
112
 
113
+ describe('getMe with a query', () => {
114
+ it('serializes the include and keeps the headers', async () => {
115
+ vi.mocked(client.get).mockResolvedValue(user)
116
+ const headers = { 'X-Custom': 'h' }
117
+ await api.getMe({ include: ['roles', 'account'] }, headers)
118
+ const [url, sent] = vi.mocked(client.get).mock.calls[0] ?? []
119
+ expect(url).toMatch(/^\/me\?/)
120
+ expect(url).toContain('include')
121
+ expect(sent).toBe(headers)
122
+ })
123
+ })
124
+
125
+ describe('getAccount with where.id', () => {
126
+ it('serializes the id of the account', async () => {
127
+ vi.mocked(client.get).mockResolvedValue(account)
128
+ await api.getAccount({ where: { id: 'acc-1' } })
129
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
130
+ expect(url).toMatch(/^\/me\/account\?/)
131
+ expect(url).toContain('acc-1')
132
+ })
133
+ })
134
+
135
+ describe('getAccountAmounts with a filter', () => {
136
+ it('sends the filter as a comma-separated list', async () => {
137
+ vi.mocked(client.get).mockResolvedValue({ accountId: 'acc-1' })
138
+ await api.getAccountAmounts({ filter: ['services.amounts', 'users.amounts'] })
139
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
140
+ expect(url).toMatch(/^\/me\/get-account-amounts\?/)
141
+ expect(decodeURIComponent(url)).toContain('services.amounts,users.amounts')
142
+ })
143
+
144
+ it('keeps the headers-only call', async () => {
145
+ const headers = { 'X-Custom': 'h' }
146
+ await api.getAccountAmounts(headers)
147
+ expect(client.get).toHaveBeenCalledWith('/me/get-account-amounts', headers)
148
+ })
149
+
150
+ it('keeps a header that comes as undefined at runtime as a header', async () => {
151
+ const headers = { Authorization: undefined } as unknown as Record<string, string>
152
+ await api.getAccountAmounts(headers)
153
+ expect(client.get).toHaveBeenCalledWith('/me/get-account-amounts', headers)
154
+ })
155
+ })
156
+
113
157
  // ── getAccount ───────────────────────────────────────────────────
114
158
 
115
159
  describe('getAccount', () => {
@@ -2,6 +2,11 @@ import { BaseApi } from '../../core/BaseApi'
2
2
  import type { Account } from '../accounts/types'
3
3
  import type { User } from '../users/types'
4
4
  import type {
5
+ AccountAmounts,
6
+ AccountAmountsQuery,
7
+ MeAccountQuery,
8
+ MeQuery,
9
+ MeStatusResponse,
5
10
  RegisterTokenPayload,
6
11
  SetOtpSecretKeyPayload,
7
12
  TakeoverPayload,
@@ -10,30 +15,74 @@ import type {
10
15
  UpdateMeStatusPayload,
11
16
  } from './types'
12
17
 
18
+ /**
19
+ * A query object always has an array or an object among its values; the headers only have strings (a header that
20
+ * comes as `undefined` at runtime stays a header). That tells the two apart in the methods that took only the
21
+ * headers before and now take a query first.
22
+ */
23
+ function splitQueryAndHeaders<TQuery extends object>(
24
+ queryOrHeaders: TQuery | Record<string, string> | undefined,
25
+ headers: Record<string, string> | undefined,
26
+ ): [TQuery | undefined, Record<string, string> | undefined] {
27
+ const isQuery =
28
+ !!queryOrHeaders &&
29
+ Object.values(queryOrHeaders).some((value) => typeof value === 'object' && value !== null)
30
+ return isQuery
31
+ ? [queryOrHeaders as TQuery, headers]
32
+ : [undefined, queryOrHeaders as Record<string, string> | undefined]
33
+ }
34
+
13
35
  export class MeApi extends BaseApi {
14
36
  /**
15
- * Returns the currently authenticated user, including their roles, departments, and timetable.
37
+ * Returns the currently authenticated user. Without `include`, the user comes without relations (roles,
38
+ * departments, timetable, account).
16
39
  * @permissions users.view
17
40
  */
18
- getMe(headers?: Record<string, string>): Promise<User> {
19
- return this.client.get<User>('/me', headers)
41
+ getMe(headers?: Record<string, string>): Promise<User>
42
+ getMe(query: MeQuery, headers?: Record<string, string>): Promise<User>
43
+ getMe(
44
+ queryOrHeaders?: MeQuery | Record<string, string>,
45
+ headers?: Record<string, string>,
46
+ ): Promise<User> {
47
+ const [query, sent] = splitQueryAndHeaders<MeQuery>(queryOrHeaders, headers)
48
+ return this.client.get<User>(`/me${this.toQueryString(query)}`, sent)
20
49
  }
21
50
 
22
- /** Returns the account associated with the currently authenticated user. */
23
- getAccount(headers?: Record<string, string>): Promise<Account> {
24
- return this.client.get<Account>('/me/account', headers)
51
+ /**
52
+ * Returns the account of the current user. Send `where.id` with the `accountId` of the user (what the web app
53
+ * does): without it, the route answers 500.
54
+ */
55
+ getAccount(headers?: Record<string, string>): Promise<Account>
56
+ getAccount(query: MeAccountQuery, headers?: Record<string, string>): Promise<Account>
57
+ getAccount(
58
+ queryOrHeaders?: MeAccountQuery | Record<string, string>,
59
+ headers?: Record<string, string>,
60
+ ): Promise<Account> {
61
+ const [query, sent] = splitQueryAndHeaders<MeAccountQuery>(queryOrHeaders, headers)
62
+ return this.client.get<Account>(`/me/account${this.toQueryString(query)}`, sent)
25
63
  }
26
64
 
27
65
  /**
28
- * Returns credit and usage amounts for the current account.
29
- * Excludes Digisac's own internal credit amounts.
66
+ * Returns the amounts of the current account named in `filter` (services and/or users). The credit amounts are
67
+ * never answered here. An account without credits control answers 400.
30
68
  */
31
- getAccountAmounts(headers?: Record<string, string>): Promise<Record<string, unknown>> {
32
- return this.client.get<Record<string, unknown>>('/me/get-account-amounts', headers)
69
+ getAccountAmounts(headers?: Record<string, string>): Promise<AccountAmounts>
70
+ getAccountAmounts(
71
+ query: AccountAmountsQuery,
72
+ headers?: Record<string, string>,
73
+ ): Promise<AccountAmounts>
74
+ getAccountAmounts(
75
+ queryOrHeaders?: AccountAmountsQuery | Record<string, string>,
76
+ headers?: Record<string, string>,
77
+ ): Promise<AccountAmounts> {
78
+ const [query, sent] = splitQueryAndHeaders<AccountAmountsQuery>(queryOrHeaders, headers)
79
+ const queryString = query ? this.toQueryString({ filter: query.filter.join(',') }) : ''
80
+ return this.client.get<AccountAmounts>(`/me/get-account-amounts${queryString}`, sent)
33
81
  }
34
82
 
35
83
  /**
36
- * Returns a URL to the Agnus billing/plan management page for the current account.
84
+ * Returns a URL to the Agnus billing/plan management page for the current account. Calls Agnus (an external
85
+ * service) and, for accounts without credits control, reports the consumption to it first.
37
86
  * @permissions myAccount.myPlan
38
87
  */
39
88
  getAgnusMyPlanUrl(headers?: Record<string, string>): Promise<{ url: string }> {
@@ -41,39 +90,45 @@ export class MeApi extends BaseApi {
41
90
  }
42
91
 
43
92
  /**
44
- * Updates the current user's profile fields such as name, language, and preferences.
45
- * If a new password is provided, it is validated against the last 6 password history entries
46
- * and all existing access tokens for the user are revoked.
93
+ * Updates the current user's profile fields such as name, language, and preferences (`data` and `preferences`
94
+ * replace the stored objects). A new `password` needs `currentPassword` and a recent login, is validated against
95
+ * the last 6 passwords, and revokes the access tokens with an expiration — including the one of this request.
47
96
  */
48
97
  updateMe(body: UpdateMePayload, headers?: Record<string, string>): Promise<User> {
49
98
  return this.client.put<User, UpdateMePayload>('/me', body, headers)
50
99
  }
51
100
 
52
- /** Updates account-level settings such as name, settings object, wizard progress, and default department. */
101
+ /**
102
+ * Updates account-level settings such as name, settings object, wizard progress, and default department. It
103
+ * applies to the whole account; the default department is required while the tickets are enabled.
104
+ */
53
105
  updateAccount(body: UpdateMeAccountPayload, headers?: Record<string, string>): Promise<Account> {
54
106
  return this.client.put<Account, UpdateMeAccountPayload>('/me/account', body, headers)
55
107
  }
56
108
 
57
109
  /**
58
- * Updates the presence status of the current user (`online`, `offline`, or `away`).
110
+ * Updates the presence status of the current user (`online`, `offline`, or `away`) for a `client`.
59
111
  * If setting to `online`, validates the user's timetable first — if outside working hours,
60
- * the status is set to `offline` instead.
112
+ * the status is set to `offline` instead. Resolves the status fields of the user.
61
113
  */
62
- updateStatus(body: UpdateMeStatusPayload, headers?: Record<string, string>): Promise<User> {
63
- return this.client.put<User, UpdateMeStatusPayload>('/me/status', body, headers)
114
+ updateStatus(
115
+ body: UpdateMeStatusPayload,
116
+ headers?: Record<string, string>,
117
+ ): Promise<MeStatusResponse> {
118
+ return this.client.put<MeStatusResponse, UpdateMeStatusPayload>('/me/status', body, headers)
64
119
  }
65
120
 
66
121
  /**
67
- * Extends the grace period for the current account when it has expired or is about to expire.
68
- * Returns the updated account.
122
+ * Extends the grace period for the current account when it has expired or is about to expire, and notifies the
123
+ * billing webhook. Returns the updated account; an account not in a grace period answers 400.
69
124
  */
70
125
  extendAccountGracePeriod(headers?: Record<string, string>): Promise<Account> {
71
126
  return this.client.post<Account>('/me/account/extend-grace-period', undefined, headers)
72
127
  }
73
128
 
74
129
  /**
75
- * Registers a OneSignal push notification token for the current user.
76
- * Used to enable push notifications on mobile/web clients.
130
+ * Registers the push notification tokens of the current user (`data` replaces the whole `data` of the user).
131
+ * Resolves with no body.
77
132
  */
78
133
  registerToken(body: RegisterTokenPayload, headers?: Record<string, string>): Promise<void> {
79
134
  return this.client.post<void, RegisterTokenPayload>(
@@ -89,8 +144,8 @@ export class MeApi extends BaseApi {
89
144
  }
90
145
 
91
146
  /**
92
- * Sets the current user's status to `online` for the given client and emits a takeover event,
93
- * which signals other connected sessions to yield control to this one.
147
+ * Sets the current user's status to `online` for the given client (`'web'` by default) and emits a takeover
148
+ * event, which signals other connected sessions to yield control to this one. Resolves with no body.
94
149
  */
95
150
  takeover(body?: TakeoverPayload, headers?: Record<string, string>): Promise<void> {
96
151
  return this.client.post<void, TakeoverPayload>('/me/takeover', body, headers)
@@ -107,9 +162,9 @@ export class MeApi extends BaseApi {
107
162
  /**
108
163
  * Activates OTP (two-factor) authentication for the current user.
109
164
  * Requires the `x-digisac-otp` header with a valid TOTP token generated from the secret key
110
- * previously set via `setOtpSecretKey`.
165
+ * previously set via `setOtpSecretKey`. Resolves the text `'OK'`.
111
166
  */
112
- activateOtpAuth(headers?: Record<string, string>): Promise<boolean> {
113
- return this.client.post<boolean>('/me/activate-otp-auth', undefined, headers)
167
+ activateOtpAuth(headers?: Record<string, string>): Promise<'OK'> {
168
+ return this.client.post<'OK'>('/me/activate-otp-auth', undefined, headers)
114
169
  }
115
170
  }
@@ -2,9 +2,68 @@ import type { AccountSettings } from '../accounts/types'
2
2
  import type { UserPreferences } from '../users/types'
3
3
  import type { UserStatus } from '../users/types'
4
4
 
5
+ /** The client of the session that sets the status: the web app or the mobile app. */
6
+ export type MeClient = 'web' | 'app'
7
+
8
+ /** Query of `GET /me`: without `include`, the user comes without relations. */
9
+ export type MeQuery = {
10
+ include?: (
11
+ | 'roles'
12
+ | 'departments'
13
+ | 'timetable'
14
+ | 'account'
15
+ | 'permissions'
16
+ | 'organizations'
17
+ | (string & {})
18
+ )[]
19
+ attributes?: string[]
20
+ }
21
+
22
+ /** Query of `GET /me/account`: `where.id` is the id of the account of the user (`accountId` of `getMe`). */
23
+ export type MeAccountQuery = {
24
+ where: { id: string }
25
+ include?: string[]
26
+ attributes?: string[]
27
+ }
28
+
29
+ export type AccountAmountsFilter = 'services.amounts' | 'users.amounts'
30
+
31
+ /** Query of `GET /me/get-account-amounts`: which amounts to answer (the credit amounts are never answered). */
32
+ export type AccountAmountsQuery = {
33
+ filter: AccountAmountsFilter[]
34
+ }
35
+
36
+ export type UsersAmounts = {
37
+ planBase: number
38
+ additional: number
39
+ totalContracted: number
40
+ planBaseConsumed: number
41
+ additionalConsumed: number
42
+ totalConsumed: number
43
+ available: number
44
+ }
45
+
46
+ export type AccountAmounts = {
47
+ accountId: string
48
+ servicesAmounts?: Record<string, unknown>
49
+ usersAmounts?: UsersAmounts
50
+ }
51
+
52
+ /** What `updateStatus` answers: the status fields of the user, not the whole user. */
53
+ export type MeStatusResponse = {
54
+ id: string
55
+ updatedAt: string
56
+ status: UserStatus
57
+ clientsStatus: Partial<Record<MeClient, UserStatus>>
58
+ language: string
59
+ account?: unknown
60
+ }
61
+
5
62
  export type UpdateMePayload = {
6
63
  name?: string
7
64
  password?: string
65
+ /** Required with `password`: the current password of the user. */
66
+ currentPassword?: string
8
67
  language?: string
9
68
  preferences?: UserPreferences
10
69
  otpAuthActive?: boolean
@@ -14,22 +73,34 @@ export type UpdateMePayload = {
14
73
 
15
74
  export type UpdateMeStatusPayload = {
16
75
  status: UserStatus
17
- client?: string
76
+ /** Required by the backend (400 without it). */
77
+ client?: MeClient
18
78
  }
19
79
 
80
+ /**
81
+ * Merged over the account. The default department is required while the tickets are enabled
82
+ * (`settings.ticketsEnabled`; 400 `DEFAULT_DEPARTMENT_REQUIRED`).
83
+ */
20
84
  export type UpdateMeAccountPayload = {
21
85
  name?: string
22
86
  settings?: Partial<AccountSettings>
23
- wizardProgress?: string
87
+ wizardProgress?: 'notStarted' | 'started' | 'finished' | (string & {})
24
88
  defaultDepartmentId?: string
89
+ defaultDepartment?: { id: string }
25
90
  }
26
91
 
27
92
  export type TakeoverPayload = {
28
- client?: string
93
+ /** Defaults to `'web'`. */
94
+ client?: MeClient
29
95
  }
30
96
 
31
97
  export type SetOtpSecretKeyPayload = {
32
98
  secretKey: string
33
99
  }
34
100
 
35
- export type RegisterTokenPayload = Record<string, unknown>
101
+ /** The push tokens of the user. `data` REPLACES the whole `data` of the user (it is not merged). */
102
+ export type RegisterTokenPayload = {
103
+ data?: { oneSignalToken?: string; expoPushToken?: string | null; fromApp?: boolean }
104
+ /** The backend ignores the call when it is `true`. */
105
+ isClientUser?: boolean
106
+ }