@ikatec/digisac-api-sdk 4.5.1 → 4.6.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 (117) hide show
  1. package/README.md +5 -3
  2. package/dist/apis/answers/AnswersApi.cjs +38 -0
  3. package/dist/apis/answers/AnswersApi.cjs.map +1 -1
  4. package/dist/apis/answers/AnswersApi.d.ts +33 -2
  5. package/dist/apis/answers/AnswersApi.d.ts.map +1 -1
  6. package/dist/apis/answers/AnswersApi.mjs +38 -0
  7. package/dist/apis/answers/AnswersApi.mjs.map +1 -1
  8. package/dist/apis/answers/types.d.ts +64 -1
  9. package/dist/apis/answers/types.d.ts.map +1 -1
  10. package/dist/apis/botSimulators/BotSimulatorsApi.cjs +42 -0
  11. package/dist/apis/botSimulators/BotSimulatorsApi.cjs.map +1 -0
  12. package/dist/apis/botSimulators/BotSimulatorsApi.d.ts +31 -0
  13. package/dist/apis/botSimulators/BotSimulatorsApi.d.ts.map +1 -0
  14. package/dist/apis/botSimulators/BotSimulatorsApi.mjs +41 -0
  15. package/dist/apis/botSimulators/BotSimulatorsApi.mjs.map +1 -0
  16. package/dist/apis/botSimulators/index.cjs +4 -0
  17. package/dist/apis/botSimulators/index.d.ts +3 -0
  18. package/dist/apis/botSimulators/index.d.ts.map +1 -0
  19. package/dist/apis/botSimulators/index.mjs +3 -0
  20. package/dist/apis/botSimulators/types.cjs +0 -0
  21. package/dist/apis/botSimulators/types.d.ts +73 -0
  22. package/dist/apis/botSimulators/types.d.ts.map +1 -0
  23. package/dist/apis/botSimulators/types.mjs +0 -0
  24. package/dist/apis/botVersions/BotVersionsApi.cjs +25 -0
  25. package/dist/apis/botVersions/BotVersionsApi.cjs.map +1 -1
  26. package/dist/apis/botVersions/BotVersionsApi.d.ts +18 -0
  27. package/dist/apis/botVersions/BotVersionsApi.d.ts.map +1 -1
  28. package/dist/apis/botVersions/BotVersionsApi.mjs +25 -0
  29. package/dist/apis/botVersions/BotVersionsApi.mjs.map +1 -1
  30. package/dist/apis/botVersions/types.d.ts +4 -0
  31. package/dist/apis/botVersions/types.d.ts.map +1 -1
  32. package/dist/apis/bots/BotsApi.cjs +99 -4
  33. package/dist/apis/bots/BotsApi.cjs.map +1 -1
  34. package/dist/apis/bots/BotsApi.d.ts +78 -5
  35. package/dist/apis/bots/BotsApi.d.ts.map +1 -1
  36. package/dist/apis/bots/BotsApi.mjs +99 -4
  37. package/dist/apis/bots/BotsApi.mjs.map +1 -1
  38. package/dist/apis/bots/types.d.ts +266 -16
  39. package/dist/apis/bots/types.d.ts.map +1 -1
  40. package/dist/apis/index.cjs +3 -0
  41. package/dist/apis/index.d.ts +1 -0
  42. package/dist/apis/index.d.ts.map +1 -1
  43. package/dist/apis/index.mjs +3 -1
  44. package/dist/apis/questions/QuestionsApi.cjs +15 -0
  45. package/dist/apis/questions/QuestionsApi.cjs.map +1 -1
  46. package/dist/apis/questions/QuestionsApi.d.ts +15 -1
  47. package/dist/apis/questions/QuestionsApi.d.ts.map +1 -1
  48. package/dist/apis/questions/QuestionsApi.mjs +15 -0
  49. package/dist/apis/questions/QuestionsApi.mjs.map +1 -1
  50. package/dist/apis/questions/types.d.ts +25 -5
  51. package/dist/apis/questions/types.d.ts.map +1 -1
  52. package/dist/apis/quickReplies/QuickRepliesApi.cjs +16 -0
  53. package/dist/apis/quickReplies/QuickRepliesApi.cjs.map +1 -1
  54. package/dist/apis/quickReplies/QuickRepliesApi.d.ts +15 -2
  55. package/dist/apis/quickReplies/QuickRepliesApi.d.ts.map +1 -1
  56. package/dist/apis/quickReplies/QuickRepliesApi.mjs +16 -0
  57. package/dist/apis/quickReplies/QuickRepliesApi.mjs.map +1 -1
  58. package/dist/apis/quickReplies/types.d.ts +30 -2
  59. package/dist/apis/quickReplies/types.d.ts.map +1 -1
  60. package/dist/apis/schedule/ScheduleApi.cjs +37 -2
  61. package/dist/apis/schedule/ScheduleApi.cjs.map +1 -1
  62. package/dist/apis/schedule/ScheduleApi.d.ts +30 -3
  63. package/dist/apis/schedule/ScheduleApi.d.ts.map +1 -1
  64. package/dist/apis/schedule/ScheduleApi.mjs +37 -2
  65. package/dist/apis/schedule/ScheduleApi.mjs.map +1 -1
  66. package/dist/apis/schedule/types.d.ts +58 -11
  67. package/dist/apis/schedule/types.d.ts.map +1 -1
  68. package/dist/apis/ticketTopics/TicketTopicsApi.cjs +25 -2
  69. package/dist/apis/ticketTopics/TicketTopicsApi.cjs.map +1 -1
  70. package/dist/apis/ticketTopics/TicketTopicsApi.d.ts +18 -4
  71. package/dist/apis/ticketTopics/TicketTopicsApi.d.ts.map +1 -1
  72. package/dist/apis/ticketTopics/TicketTopicsApi.mjs +25 -2
  73. package/dist/apis/ticketTopics/TicketTopicsApi.mjs.map +1 -1
  74. package/dist/apis/ticketTopics/types.d.ts +22 -1
  75. package/dist/apis/ticketTopics/types.d.ts.map +1 -1
  76. package/dist/core/BaseCrudApi.cjs +5 -2
  77. package/dist/core/BaseCrudApi.cjs.map +1 -1
  78. package/dist/core/BaseCrudApi.d.ts +7 -4
  79. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  80. package/dist/core/BaseCrudApi.mjs +5 -2
  81. package/dist/core/BaseCrudApi.mjs.map +1 -1
  82. package/dist/core/types.d.ts +1 -1
  83. package/dist/core/types.d.ts.map +1 -1
  84. package/dist/incommingWebhooks/index.d.ts +7 -7
  85. package/dist/incommingWebhooks/index.d.ts.map +1 -1
  86. package/package.json +1 -1
  87. package/src/apis/answers/AnswersApi.test.ts +67 -0
  88. package/src/apis/answers/AnswersApi.ts +66 -2
  89. package/src/apis/answers/types.ts +64 -2
  90. package/src/apis/botSimulators/BotSimulatorsApi.test.ts +58 -0
  91. package/src/apis/botSimulators/BotSimulatorsApi.ts +57 -0
  92. package/src/apis/botSimulators/index.ts +2 -0
  93. package/src/apis/botSimulators/types.ts +72 -0
  94. package/src/apis/botVersions/BotVersionsApi.test.ts +51 -0
  95. package/src/apis/botVersions/BotVersionsApi.ts +41 -0
  96. package/src/apis/botVersions/types.ts +4 -0
  97. package/src/apis/bots/BotsApi.test.ts +177 -0
  98. package/src/apis/bots/BotsApi.ts +162 -6
  99. package/src/apis/bots/types.ts +267 -15
  100. package/src/apis/index.ts +1 -0
  101. package/src/apis/payloads.test-d.ts +148 -0
  102. package/src/apis/questions/QuestionsApi.test.ts +49 -0
  103. package/src/apis/questions/QuestionsApi.ts +20 -1
  104. package/src/apis/questions/types.ts +25 -6
  105. package/src/apis/quickReplies/QuickRepliesApi.test.ts +69 -0
  106. package/src/apis/quickReplies/QuickRepliesApi.ts +35 -2
  107. package/src/apis/quickReplies/types.ts +34 -2
  108. package/src/apis/schedule/ScheduleApi.test.ts +80 -0
  109. package/src/apis/schedule/ScheduleApi.ts +59 -6
  110. package/src/apis/schedule/types.ts +66 -11
  111. package/src/apis/ticketTopics/TicketTopicsApi.test.ts +40 -0
  112. package/src/apis/ticketTopics/TicketTopicsApi.ts +35 -4
  113. package/src/apis/ticketTopics/types.ts +26 -1
  114. package/src/core/BaseCrudApi.ts +9 -5
  115. package/src/core/types.ts +2 -1
  116. package/src/incommingWebhooks/index.test-d.ts +9 -0
  117. package/src/incommingWebhooks/index.ts +7 -7
@@ -0,0 +1,69 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { QuickRepliesApi } from './QuickRepliesApi'
4
+ import type { QuickRepliesLoadOnDemandQuery } from './types'
5
+
6
+ // ── Mock client factory ──────────────────────────────────────────────
7
+
8
+ function createMockClient(): ApiClient {
9
+ return {
10
+ setAccessToken: vi.fn().mockReturnThis(),
11
+ request: vi.fn(),
12
+ get: vi.fn(),
13
+ post: vi.fn(),
14
+ put: vi.fn(),
15
+ patch: vi.fn(),
16
+ delete: vi.fn(),
17
+ }
18
+ }
19
+
20
+ const headers = { 'X-Custom': 'value' }
21
+
22
+ // ── Tests ────────────────────────────────────────────────────────────
23
+
24
+ describe('QuickRepliesApi', () => {
25
+ let client: ReturnType<typeof createMockClient>
26
+ let api: QuickRepliesApi
27
+
28
+ beforeEach(() => {
29
+ client = createMockClient()
30
+ api = new QuickRepliesApi(client)
31
+ })
32
+
33
+ it('loadOnDemand calls client.get with /quick-replies/load-on-demand and the query', async () => {
34
+ const query: QuickRepliesLoadOnDemandQuery = { order: [['title', 'ASC']], limit: 20, offset: 0 }
35
+ vi.mocked(client.get).mockResolvedValue({ data: [], total: 0 })
36
+ expect(await api.loadOnDemand(query, headers)).toEqual({
37
+ data: [],
38
+ total: 0,
39
+ })
40
+ expect(client.get).toHaveBeenCalledWith(
41
+ `/quick-replies/load-on-demand?query=${encodeURIComponent(JSON.stringify(query))}`,
42
+ headers,
43
+ )
44
+ })
45
+
46
+ it('create sends the departments, categories and files', async () => {
47
+ const body = {
48
+ title: 'Saudação',
49
+ text: 'Olá!',
50
+ departmentIds: ['d1'],
51
+ categoryIds: ['c1'],
52
+ files: [{ id: 'f1' }],
53
+ }
54
+ await api.create(body)
55
+ expect(client.post).toHaveBeenCalledWith('/quick-replies', body, undefined)
56
+ })
57
+
58
+ it('updateById sends the whole reply', async () => {
59
+ const body = { title: 'Saudação', text: 'Oi!', files: [] }
60
+ await api.updateById('q1', body, headers)
61
+ expect(client.put).toHaveBeenCalledWith('/quick-replies/q1', body, headers)
62
+ })
63
+
64
+ it('deleteById resolves to the OK of the route', async () => {
65
+ vi.mocked(client.delete).mockResolvedValue('OK')
66
+ expect(await api.deleteById('q1')).toBe('OK')
67
+ expect(client.delete).toHaveBeenCalledWith('/quick-replies/q1', undefined)
68
+ })
69
+ })
@@ -1,13 +1,46 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { QuickReply, CreateQuickReplyPayload, UpdateQuickReplyPayload } from './types'
3
+ import type { ListQuery } from '../../core/types'
4
+ import type {
5
+ QuickReply,
6
+ QuickRepliesLoadOnDemandQuery,
7
+ QuickRepliesLoadOnDemandResult,
8
+ CreateQuickReplyPayload,
9
+ UpdateQuickReplyPayload,
10
+ } from './types'
4
11
 
5
12
  export class QuickRepliesApi extends BaseCrudApi<
6
13
  QuickReply,
7
14
  CreateQuickReplyPayload,
8
- UpdateQuickReplyPayload
15
+ UpdateQuickReplyPayload,
16
+ ListQuery<QuickReply>,
17
+ 'OK'
9
18
  > {
10
19
  constructor(client: ApiClient) {
11
20
  super(client, '/quick-replies')
12
21
  }
22
+
23
+ /**
24
+ * Deletes a quick reply (soft delete). Resolves to `'OK'`; an unknown id answers 500.
25
+ * @permissions quickReplies.view OR quickReplies.destroy
26
+ */
27
+ override deleteById(id: string, headers?: Record<string, string>): Promise<'OK'> {
28
+ return super.deleteById(id, headers)
29
+ }
30
+
31
+ /**
32
+ * Lists the replies the user can use (no department, or one of the departments of the user),
33
+ * the way the chat loads them: `limit`/`offset` instead of pages, and `{ data, total }`
34
+ * instead of the pagination envelope.
35
+ * @permissions quickReplies.view
36
+ */
37
+ loadOnDemand(
38
+ query?: QuickRepliesLoadOnDemandQuery,
39
+ headers?: Record<string, string>,
40
+ ): Promise<QuickRepliesLoadOnDemandResult> {
41
+ return this.client.get<QuickRepliesLoadOnDemandResult>(
42
+ `${this.basePath}/load-on-demand${this.toQueryString(query)}`,
43
+ headers,
44
+ )
45
+ }
13
46
  }
@@ -1,3 +1,4 @@
1
+ import type { ListQuery } from '../../core/types'
1
2
  import type { Category } from '../categories/types'
2
3
  import type { Department } from '../departments/types'
3
4
  import type { File } from '../files/types'
@@ -11,6 +12,7 @@ export type QuickReply = {
11
12
  updatedAt: string
12
13
  deletedAt: string | null
13
14
  // relationships
15
+ /** Empty = all departments. */
14
16
  departments?: Department[]
15
17
  categories?: Category[]
16
18
  files?: File[]
@@ -18,12 +20,42 @@ export type QuickReply = {
18
20
 
19
21
  export type QuickReplyRelationships = 'departments' | 'categories' | 'files'
20
22
 
23
+ /**
24
+ * Files already uploaded through `FilesApi` with `attachedType: 'quickreply.file'` — uploaded
25
+ * with another type they get the `attachedId` but never show in `files`.
26
+ */
27
+ export type QuickReplyFilePayload = { id: string }
28
+
29
+ /** `title` and `text` are each unique in the account (409, unless the account allows duplicate names). */
21
30
  export type CreateQuickReplyPayload = {
22
31
  title: string
23
32
  text: string
33
+ /** Department ids; omitted = all departments. */
34
+ departmentIds?: string[]
35
+ categoryIds?: string[]
36
+ files?: QuickReplyFilePayload[]
24
37
  }
25
38
 
39
+ /**
40
+ * Not partial: `title` and `text` are required, the departments and categories are replaced
41
+ * (omitted = none), and so are the files.
42
+ */
26
43
  export type UpdateQuickReplyPayload = {
27
- title?: string
28
- text?: string
44
+ title: string
45
+ text: string
46
+ departmentIds?: string[]
47
+ categoryIds?: string[]
48
+ /** The files to keep or attach; `[]` removes all. Required: omitted, the update fails (500). */
49
+ files: QuickReplyFilePayload[]
50
+ }
51
+
52
+ /** The query of `loadOnDemand`, passed to the database as is. */
53
+ export type QuickRepliesLoadOnDemandQuery = Pick<
54
+ ListQuery<QuickReply>,
55
+ 'where' | 'include' | 'order' | 'attributes' | 'limit' | 'offset'
56
+ >
57
+
58
+ export type QuickRepliesLoadOnDemandResult = {
59
+ data: QuickReply[]
60
+ total: number
29
61
  }
@@ -0,0 +1,80 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { ScheduleApi } from './ScheduleApi'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ const headers = { 'X-Custom': 'value' }
20
+ const body = {
21
+ scheduledAt: '2026-10-02T12:00:00.000Z',
22
+ contactId: 'c1',
23
+ departmentId: 'd1',
24
+ message: 'Lembrete',
25
+ notes: '',
26
+ }
27
+
28
+ // ── Tests ────────────────────────────────────────────────────────────
29
+
30
+ describe('ScheduleApi', () => {
31
+ let client: ReturnType<typeof createMockClient>
32
+ let api: ScheduleApi
33
+
34
+ beforeEach(() => {
35
+ client = createMockClient()
36
+ api = new ScheduleApi(client)
37
+ })
38
+
39
+ it('create calls client.post with /schedule and resolves with no content', async () => {
40
+ vi.mocked(client.post).mockResolvedValue(undefined)
41
+ expect(await api.create(body, headers)).toBeUndefined()
42
+ expect(client.post).toHaveBeenCalledWith('/schedule', body, headers)
43
+ })
44
+
45
+ it('updateById calls client.put with /schedule/:id', async () => {
46
+ await api.updateById('s1', { ...body, files: [{ id: 'f1' }] })
47
+ expect(client.put).toHaveBeenCalledWith(
48
+ '/schedule/s1',
49
+ { ...body, files: [{ id: 'f1' }] },
50
+ undefined,
51
+ )
52
+ })
53
+
54
+ it('deleteById calls client.delete with /schedule/:id', async () => {
55
+ vi.mocked(client.delete).mockResolvedValue('OK')
56
+ expect(await api.deleteById('s1', headers)).toBe('OK')
57
+ expect(client.delete).toHaveBeenCalledWith('/schedule/s1', headers)
58
+ })
59
+
60
+ it('deleteMany sends the ids in where.id of a DELETE body', async () => {
61
+ vi.mocked(client.request).mockResolvedValue('OK')
62
+ expect(await api.deleteMany(['s1', 's2'], headers)).toBe('OK')
63
+ expect(client.request).toHaveBeenCalledWith(
64
+ 'DELETE',
65
+ '/schedule/many',
66
+ { where: { id: ['s1', 's2'] } },
67
+ headers,
68
+ )
69
+ })
70
+
71
+ it('reads through the inherited getMany', async () => {
72
+ await api.getMany({ where: { contactId: 'c1' }, paginate: false })
73
+ expect(client.get).toHaveBeenCalledWith(
74
+ `/schedule?query=${encodeURIComponent(
75
+ JSON.stringify({ where: { contactId: 'c1' }, paginate: false }),
76
+ )}`,
77
+ undefined,
78
+ )
79
+ })
80
+ })
@@ -1,13 +1,66 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
- import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { Schedule, CreateSchedulePayload, UpdateSchedulePayload } from './types'
4
-
5
- export class ScheduleApi extends BaseCrudApi<
2
+ import { BaseReadApi } from '../../core/BaseReadApi'
3
+ import type {
6
4
  Schedule,
5
+ ScheduleRecord,
7
6
  CreateSchedulePayload,
8
- UpdateSchedulePayload
9
- > {
7
+ UpdateSchedulePayload,
8
+ } from './types'
9
+
10
+ /**
11
+ * Scheduled messages. Create answers no body and update answers the stored record, so this
12
+ * class extends `BaseReadApi` and writes its own create/update/delete.
13
+ */
14
+ export class ScheduleApi extends BaseReadApi<Schedule> {
10
15
  constructor(client: ApiClient) {
11
16
  super(client, '/schedule')
12
17
  }
18
+
19
+ /**
20
+ * Creates a schedule. Resolves with NO CONTENT: find it back with `getOne` (e.g. by
21
+ * `contactId` and `scheduledAt`).
22
+ * @permissions schedule.create
23
+ */
24
+ create(body: CreateSchedulePayload, headers?: Record<string, string>): Promise<void> {
25
+ return this.client.post<void, CreateSchedulePayload>(this.basePath, body, headers)
26
+ }
27
+
28
+ /**
29
+ * Updates a schedule. Resolves to the stored record, without relationships. An unknown id
30
+ * answers 500.
31
+ * @permissions schedule.view OR schedule.update
32
+ */
33
+ updateById(
34
+ id: string,
35
+ body: UpdateSchedulePayload,
36
+ headers?: Record<string, string>,
37
+ ): Promise<ScheduleRecord> {
38
+ return this.client.put<ScheduleRecord, UpdateSchedulePayload>(
39
+ `${this.basePath}/${id}`,
40
+ body,
41
+ headers,
42
+ )
43
+ }
44
+
45
+ /**
46
+ * Deletes a schedule (soft delete). Resolves to `'OK'`; an unknown id answers 500.
47
+ * @permissions schedule.view OR schedule.destroy
48
+ */
49
+ deleteById(id: string, headers?: Record<string, string>): Promise<'OK'> {
50
+ return this.client.delete<'OK'>(`${this.basePath}/${id}`, headers)
51
+ }
52
+
53
+ /**
54
+ * Deletes several schedules of the account. Ids that are not UUIDs, or of another account, are
55
+ * skipped in silence; always resolves to `'OK'`.
56
+ * @permissions schedule.destroy
57
+ */
58
+ deleteMany(ids: string[], headers?: Record<string, string>): Promise<'OK'> {
59
+ return this.client.request<'OK', { where: { id: string[] } }>(
60
+ 'DELETE',
61
+ `${this.basePath}/many`,
62
+ { where: { id: ids } },
63
+ headers,
64
+ )
65
+ }
13
66
  }
@@ -1,15 +1,28 @@
1
1
  import type { Contact } from '../contacts/types'
2
2
  import type { Department } from '../departments/types'
3
+ import type { File } from '../files/types'
3
4
  import type { User } from '../users/types'
4
5
 
6
+ /** `scheduled` until the worker runs it; `error` and `not_sent` come with the `reason`. */
7
+ export type ScheduleStatus = 'scheduled' | 'done' | 'error' | 'not_sent'
8
+
5
9
  export type Schedule = {
6
10
  id: string
7
- status: string
11
+ status: ScheduleStatus
12
+ /**
13
+ * Why it was not sent: `ALREADY_TICKET_OPENED`, `DDD_BLOCKED`, `SERVICE_IS_NOT_ENABLED`,
14
+ * `CONTACT_IS_BLOCKED`, `CONTACT_NOT_EXISTS`, `HSM_LIMIT_EXCEEDED`, `HSM_IS_BLOCKED`,
15
+ * `SERVICE_IS_BLOCKED`, `INVALID_PAYLOAD`, a WhatsApp Business error code or
16
+ * `SCHEDULE_UNKNOWN_REASON`.
17
+ */
8
18
  reason: string | null
9
- message: string
19
+ /** For a WhatsApp Business template (`hsmId`), the message the server built from it. */
20
+ message: string | null
10
21
  notes: string
11
- openTicket: boolean
12
- notificateUser: boolean
22
+ openTicket: boolean | null
23
+ notificateUser: boolean | null
24
+ /** Sends the message even when the contact already has an open ticket. */
25
+ sendWithOpenTicket: boolean | null
13
26
  extraOptions: Record<string, unknown> | null
14
27
  hsmId: string | null
15
28
  hsmFileId: string | null
@@ -20,39 +33,81 @@ export type Schedule = {
20
33
  accountId: string
21
34
  createdAt: string
22
35
  updatedAt: string
23
- deletedAt: string | null
24
36
  // relationships
25
37
  contact?: Contact
26
38
  department?: Department
27
39
  user?: User | null
40
+ files?: File[]
41
+ /** The header media of the WhatsApp Business template. */
42
+ hsmFile?: File
43
+ }
44
+
45
+ /** The schedule as stored, without relationships: what `updateById` resolves to. */
46
+ export type ScheduleRecord = Omit<
47
+ Schedule,
48
+ 'contact' | 'department' | 'user' | 'files' | 'hsmFile'
49
+ > & {
50
+ deletedAt: string | null
28
51
  }
29
52
 
30
- export type ScheduleRelationships = 'contact' | 'department' | 'user'
53
+ export type ScheduleRelationships =
54
+ | 'contact'
55
+ | 'department'
56
+ | 'user'
57
+ | 'files'
58
+ | 'hsmFile'
59
+ | 'contact.service'
60
+ | 'contact.person'
61
+
62
+ /** A new attachment. */
63
+ export type ScheduleNewFilePayload = { base64: string; name: string; mimetype: string }
31
64
 
65
+ /** On update: a new attachment, or one to keep (`id`). */
66
+ export type ScheduleFilePayload = ScheduleNewFilePayload | { id: string }
67
+
68
+ /**
69
+ * Another schedule of the connection less than 5 minutes apart answers 402
70
+ * (`Schedule time is already exist.`). A contact of another account answers 500.
71
+ */
32
72
  export type CreateSchedulePayload = {
33
73
  scheduledAt: string
34
74
  contactId: string
35
75
  departmentId: string
36
- message: string
76
+ /** Up to 1024 characters. Optional for a WhatsApp Business template, built from `hsmId`. */
77
+ message?: string
78
+ /**
79
+ * Up to 1024 characters. Required by the route only with `openTicket: true`, but the column is
80
+ * NOT NULL: send it always (an empty string is fine).
81
+ */
37
82
  notes: string
38
- openTicket: boolean
83
+ openTicket?: boolean
39
84
  notificateUser?: boolean
85
+ sendWithOpenTicket?: boolean
40
86
  userId?: string
41
87
  hsmId?: string
42
88
  hsmFileId?: string
43
89
  extraOptions?: Record<string, unknown>
90
+ /** New attachments only: the create drops `{ id }` items. */
91
+ files?: ScheduleNewFilePayload[]
44
92
  }
45
93
 
94
+ /**
95
+ * Not partial: `scheduledAt`, `contactId` and `departmentId` are required on update too. The
96
+ * 5-minute conflict (402) is checked against the whole account.
97
+ */
46
98
  export type UpdateSchedulePayload = {
47
- scheduledAt?: string
48
- contactId?: string
49
- departmentId?: string
99
+ scheduledAt: string
100
+ contactId: string
101
+ departmentId: string
50
102
  userId?: string | null
51
103
  message?: string
52
104
  notes?: string
53
105
  openTicket?: boolean
54
106
  notificateUser?: boolean
107
+ sendWithOpenTicket?: boolean
55
108
  hsmId?: string | null
56
109
  hsmFileId?: string | null
57
110
  extraOptions?: Record<string, unknown> | null
111
+ /** Replaces the attachments: `{ id }` keeps one, `{ base64, ... }` adds one; omitted or `[]` removes all. */
112
+ files?: ScheduleFilePayload[]
58
113
  }
@@ -55,4 +55,44 @@ describe('TicketTopicsApi', () => {
55
55
  await unarchive('t1')
56
56
  expect(client.post).toHaveBeenCalledTimes(2)
57
57
  })
58
+
59
+ it('create sends the department ids', async () => {
60
+ await api.create({ name: 'Dúvida', departments: ['d1'] })
61
+ expect(client.post).toHaveBeenCalledWith(
62
+ '/ticket-topics',
63
+ { name: 'Dúvida', departments: ['d1'] },
64
+ undefined,
65
+ )
66
+ })
67
+
68
+ it('getMany sends the department filters at the top of the query', async () => {
69
+ const query = { departmentIds: ['d1'], includeNoDepartment: true }
70
+ await api.getMany(query)
71
+ expect(client.get).toHaveBeenCalledWith(
72
+ `/ticket-topics?query=${encodeURIComponent(JSON.stringify(query))}`,
73
+ undefined,
74
+ )
75
+ })
76
+
77
+ it('deleteById resolves to the OK of the route', async () => {
78
+ vi.mocked(client.delete).mockResolvedValue('OK')
79
+ expect(await api.deleteById('t1')).toBe('OK')
80
+ expect(client.delete).toHaveBeenCalledWith('/ticket-topics/t1', undefined)
81
+ })
82
+
83
+ it('getOne reads one item of the paginated list, where the department filters apply', async () => {
84
+ vi.mocked(client.get).mockResolvedValue({ data: [{ id: 't1' }], total: 1 })
85
+ expect(await api.getOne({ departmentIds: ['d1'] })).toEqual({ id: 't1' })
86
+ expect(client.get).toHaveBeenCalledWith(
87
+ `/ticket-topics?query=${encodeURIComponent(
88
+ JSON.stringify({ departmentIds: ['d1'], paginate: true, perPage: 1, page: 1 }),
89
+ )}`,
90
+ undefined,
91
+ )
92
+ })
93
+
94
+ it('getOne resolves null on an empty page', async () => {
95
+ vi.mocked(client.get).mockResolvedValue({ data: [], total: 0 })
96
+ expect(await api.getOne()).toBeNull()
97
+ })
58
98
  })
@@ -1,11 +1,18 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { TicketTopic, CreateTicketTopicPayload, UpdateTicketTopicPayload } from './types'
3
+ import type {
4
+ TicketTopic,
5
+ TicketTopicListQuery,
6
+ CreateTicketTopicPayload,
7
+ UpdateTicketTopicPayload,
8
+ } from './types'
4
9
 
5
10
  export class TicketTopicsApi extends BaseCrudApi<
6
11
  TicketTopic,
7
12
  CreateTicketTopicPayload,
8
- UpdateTicketTopicPayload
13
+ UpdateTicketTopicPayload,
14
+ TicketTopicListQuery,
15
+ 'OK'
9
16
  > {
10
17
  constructor(client: ApiClient) {
11
18
  super(client, '/ticket-topics')
@@ -13,10 +20,34 @@ export class TicketTopicsApi extends BaseCrudApi<
13
20
  this.unarchive = this.unarchive.bind(this)
14
21
  }
15
22
 
23
+ /**
24
+ * Returns the first topic matching the query, or `null` when none match. Reads one item of the
25
+ * paginated list (instead of `paginate: false`), the only path where `departmentIds` and
26
+ * `includeNoDepartment` apply.
27
+ * @permissions ticketTopics.view
28
+ */
29
+ override async getOne(
30
+ query?: TicketTopicListQuery,
31
+ headers?: Record<string, string>,
32
+ ): Promise<TicketTopic | null> {
33
+ const page = await this.getMany({ ...query, paginate: true, perPage: 1, page: 1 }, headers)
34
+ return page?.data?.[0] ?? null
35
+ }
36
+
37
+ /**
38
+ * Deletes a ticket topic (soft delete). Resolves to `'OK'`; an unknown id answers 500.
39
+ * @permissions ticketTopics.view OR ticketTopics.destroy
40
+ */
41
+ override deleteById(id: string, headers?: Record<string, string>): Promise<'OK'> {
42
+ return super.deleteById(id, headers)
43
+ }
44
+
16
45
  /**
17
46
  * Archives a ticket topic. Unlike most resources, ticket-topics archive through a
18
47
  * dedicated endpoint (POST `${basePath}/${id}/archive`) rather than `updateById`.
19
- * @permissions ticketTopic.update
48
+ * Resolves to the stored topic, without `departments`. A topic used in a bot answers 403;
49
+ * an unknown id answers 500.
50
+ * @permissions ticketTopics.archive
20
51
  */
21
52
  archive(id: string, headers?: Record<string, string>): Promise<TicketTopic> {
22
53
  return this.client.post<TicketTopic>(
@@ -28,7 +59,7 @@ export class TicketTopicsApi extends BaseCrudApi<
28
59
 
29
60
  /**
30
61
  * Restores a previously archived ticket topic via the same dedicated endpoint.
31
- * @permissions ticketTopic.update
62
+ * @permissions ticketTopics.archive
32
63
  */
33
64
  unarchive(id: string, headers?: Record<string, string>): Promise<TicketTopic> {
34
65
  return this.client.post<TicketTopic>(
@@ -1,3 +1,6 @@
1
+ import type { ListQuery } from '../../core/types'
2
+ import type { Department } from '../departments/types'
3
+
1
4
  export type TicketTopic = {
2
5
  id: string
3
6
  name: string
@@ -6,12 +9,34 @@ export type TicketTopic = {
6
9
  createdAt: string
7
10
  updatedAt: string
8
11
  deletedAt: string | null
12
+ // relationships
13
+ /** Always comes in the paginated `getMany`; elsewhere only with `include`. Empty = all departments. */
14
+ departments?: Department[]
15
+ }
16
+
17
+ export type TicketTopicRelationships = 'departments'
18
+
19
+ /**
20
+ * Department filters of the paginated `getMany` and of `getOne`. `getMany({ paginate: false })`
21
+ * ignores them in silence (the route reads them only on the paginated path).
22
+ */
23
+ export type TicketTopicListQuery = ListQuery<TicketTopic> & {
24
+ /** Topics linked to any of these departments. */
25
+ departmentIds?: string | string[]
26
+ /** Also (or only) the topics linked to no department. Only with the default `'json'` query format. */
27
+ includeNoDepartment?: boolean
9
28
  }
10
29
 
11
30
  export type CreateTicketTopicPayload = {
31
+ /** Unique in the account (409, unless the account allows duplicate names); trimmed. */
12
32
  name: string
33
+ /** Department ids; `[]` = all departments. Required: a missing list answers 500 after creating the topic. */
34
+ departments: string[]
13
35
  }
14
36
 
15
37
  export type UpdateTicketTopicPayload = {
16
- name?: string
38
+ /** Required on update too. */
39
+ name: string
40
+ /** Department ids, replacing the current ones; omitted keeps them. */
41
+ departments?: string[]
17
42
  }
@@ -14,15 +14,19 @@ export type {
14
14
  CrudApi,
15
15
  } from './types'
16
16
 
17
- /** Generic CRUD resource: the reads of `BaseReadApi` plus `create`, `updateById` and `deleteById`. */
17
+ /**
18
+ * Generic CRUD resource: the reads of `BaseReadApi` plus `create`, `updateById` and `deleteById`.
19
+ * `TDeleteResponse` is what `deleteById` resolves to (most routes answer the text `'OK'`).
20
+ */
18
21
  export class BaseCrudApi<
19
22
  TResponse,
20
23
  TCreate,
21
24
  TUpdate = TCreate,
22
25
  TQuery extends ListQuery<TResponse> = ListQuery<TResponse>,
26
+ TDeleteResponse = TResponse,
23
27
  >
24
28
  extends BaseReadApi<TResponse, TQuery>
25
- implements CrudApi<TResponse, TCreate, TUpdate, TQuery>
29
+ implements CrudApi<TResponse, TCreate, TUpdate, TQuery, TDeleteResponse>
26
30
  {
27
31
  /**
28
32
  * Creates a new resource.
@@ -42,9 +46,9 @@ export class BaseCrudApi<
42
46
 
43
47
  /**
44
48
  * Deletes a resource by ID.
45
- * @permissions $resourceName.delete
49
+ * @permissions $resourceName.destroy
46
50
  */
47
- deleteById(id: string, headers?: Record<string, string>): Promise<TResponse> {
48
- return this.client.delete<TResponse>(`${this.basePath}/${id}`, headers)
51
+ deleteById(id: string, headers?: Record<string, string>): Promise<TDeleteResponse> {
52
+ return this.client.delete<TDeleteResponse>(`${this.basePath}/${id}`, headers)
49
53
  }
50
54
  }
package/src/core/types.ts CHANGED
@@ -159,8 +159,9 @@ export type CrudApi<
159
159
  TCreate,
160
160
  TUpdate = TCreate,
161
161
  TQuery extends ListQuery<TResponse> = ListQuery<TResponse>,
162
+ TDeleteResponse = TResponse,
162
163
  > = HasGetMany<TResponse, TQuery> &
163
164
  HasGetById<TResponse> &
164
165
  HasCreate<TResponse, TCreate> &
165
166
  HasUpdateById<TResponse, TUpdate> &
166
- HasDeleteById<TResponse>
167
+ HasDeleteById<TDeleteResponse>
@@ -2,6 +2,7 @@ import { expectAssignable, expectNotAssignable, expectType } from 'tsd'
2
2
  import type { WebhookPayload, WebhookEvent, WebhookEventPayloadMap, BotCommandData } from './index'
3
3
  import type { Message, MessageRelationships } from '../apis/messages'
4
4
  import type { Ticket, TicketRelationships } from '../apis/tickets'
5
+ import type { BotApiSignalData, BotContexts } from '../apis/bots'
5
6
 
6
7
  // ── WebhookPayload (generic) ─────────────────────────────────────────────────
7
8
 
@@ -37,6 +38,14 @@ declare const botPayload: WebhookPayload<'bot.command'>
37
38
  expectType<BotCommandData>(botPayload.data)
38
39
  expectAssignable<{ id: string; command: string }>(botPayload.data)
39
40
 
41
+ // ── bot.* deliver the stored bot, with the contexts in the map form ──────────
42
+
43
+ declare const botCreated: WebhookPayload<'bot.created'>
44
+ expectType<BotContexts>(botCreated.data.contexts)
45
+
46
+ declare const apiSignal: WebhookPayload<'bot.api-signal'>
47
+ expectType<BotApiSignalData>(apiSignal.data)
48
+
40
49
  // ── invalid events are rejected ──────────────────────────────────────────────
41
50
 
42
51
  expectNotAssignable<WebhookEvent>('message.deleted')