@ikatec/digisac-api-sdk 4.4.1 → 4.5.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 (49) hide show
  1. package/dist/apis/cards/CardsApi.cjs +70 -0
  2. package/dist/apis/cards/CardsApi.cjs.map +1 -1
  3. package/dist/apis/cards/CardsApi.d.ts +53 -1
  4. package/dist/apis/cards/CardsApi.d.ts.map +1 -1
  5. package/dist/apis/cards/CardsApi.mjs +70 -0
  6. package/dist/apis/cards/CardsApi.mjs.map +1 -1
  7. package/dist/apis/cards/types.d.ts +84 -0
  8. package/dist/apis/cards/types.d.ts.map +1 -1
  9. package/dist/apis/kanbanBoards/types.d.ts +33 -4
  10. package/dist/apis/kanbanBoards/types.d.ts.map +1 -1
  11. package/dist/apis/kanbanCards/KanbanCardsApi.cjs +8 -0
  12. package/dist/apis/kanbanCards/KanbanCardsApi.cjs.map +1 -1
  13. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts +7 -1
  14. package/dist/apis/kanbanCards/KanbanCardsApi.d.ts.map +1 -1
  15. package/dist/apis/kanbanCards/KanbanCardsApi.mjs +8 -0
  16. package/dist/apis/kanbanCards/KanbanCardsApi.mjs.map +1 -1
  17. package/dist/apis/kanbanCards/types.d.ts +17 -5
  18. package/dist/apis/kanbanCards/types.d.ts.map +1 -1
  19. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs +9 -0
  20. package/dist/apis/kanbanColumns/KanbanColumnsApi.cjs.map +1 -1
  21. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts +8 -1
  22. package/dist/apis/kanbanColumns/KanbanColumnsApi.d.ts.map +1 -1
  23. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs +9 -0
  24. package/dist/apis/kanbanColumns/KanbanColumnsApi.mjs.map +1 -1
  25. package/dist/apis/kanbanColumns/types.d.ts +14 -3
  26. package/dist/apis/kanbanColumns/types.d.ts.map +1 -1
  27. package/dist/apis/pipeline/PipelineApi.cjs +133 -0
  28. package/dist/apis/pipeline/PipelineApi.cjs.map +1 -1
  29. package/dist/apis/pipeline/PipelineApi.d.ts +102 -1
  30. package/dist/apis/pipeline/PipelineApi.d.ts.map +1 -1
  31. package/dist/apis/pipeline/PipelineApi.mjs +133 -0
  32. package/dist/apis/pipeline/PipelineApi.mjs.map +1 -1
  33. package/dist/apis/pipeline/types.d.ts +164 -0
  34. package/dist/apis/pipeline/types.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/apis/cards/CardsApi.test.ts +128 -0
  37. package/src/apis/cards/CardsApi.ts +124 -1
  38. package/src/apis/cards/types.ts +81 -0
  39. package/src/apis/kanbanBoards/types.ts +36 -4
  40. package/src/apis/kanbanCards/KanbanCardsApi.test.ts +46 -0
  41. package/src/apis/kanbanCards/KanbanCardsApi.ts +23 -1
  42. package/src/apis/kanbanCards/types.ts +18 -5
  43. package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +46 -0
  44. package/src/apis/kanbanColumns/KanbanColumnsApi.ts +24 -1
  45. package/src/apis/kanbanColumns/types.ts +15 -3
  46. package/src/apis/payloads.test-d.ts +30 -0
  47. package/src/apis/pipeline/PipelineApi.test.ts +193 -0
  48. package/src/apis/pipeline/PipelineApi.ts +274 -1
  49. package/src/apis/pipeline/types.ts +162 -0
@@ -1,6 +1,11 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { KanbanCard, CreateKanbanCardPayload, UpdateKanbanCardPayload } from './types'
3
+ import type {
4
+ KanbanCard,
5
+ CreateKanbanCardPayload,
6
+ ReorderKanbanCardPayload,
7
+ UpdateKanbanCardPayload,
8
+ } from './types'
4
9
 
5
10
  export class KanbanCardsApi extends BaseCrudApi<
6
11
  KanbanCard,
@@ -10,4 +15,21 @@ export class KanbanCardsApi extends BaseCrudApi<
10
15
  constructor(client: ApiClient) {
11
16
  super(client, '/kanban-cards')
12
17
  }
18
+
19
+ /**
20
+ * Moves a card within its column or to another column (see {@link ReorderKanbanCardPayload}).
21
+ * Resolves to the card with its new `position` and `columnId`.
22
+ * @permissions kanban.cards.move
23
+ */
24
+ reorder(
25
+ id: string,
26
+ body: ReorderKanbanCardPayload,
27
+ headers?: Record<string, string>,
28
+ ): Promise<KanbanCard> {
29
+ return this.client.patch<KanbanCard, ReorderKanbanCardPayload>(
30
+ `${this.basePath}/${id}/reorder`,
31
+ body,
32
+ headers,
33
+ )
34
+ }
13
35
  }
@@ -3,7 +3,6 @@ import type { Ticket } from '../tickets/types'
3
3
 
4
4
  export type KanbanCard = {
5
5
  id: string
6
- ticketSubject: string | null
7
6
  position: string
8
7
  columnId: string
9
8
  ticketId: string
@@ -12,10 +11,9 @@ export type KanbanCard = {
12
11
  lastColumnChangeAt: string | null
13
12
  createdAt: string
14
13
  updatedAt: string
15
- deletedAt: string | null
16
14
  // relationships
17
- column?: KanbanColumn
18
- ticket?: Ticket
15
+ column?: KanbanColumn | null
16
+ ticket?: Ticket | null
19
17
  }
20
18
 
21
19
  export type KanbanCardRelationships = 'column' | 'ticket'
@@ -29,5 +27,20 @@ export type CreateKanbanCardPayload = {
29
27
  export type UpdateKanbanCardPayload = {
30
28
  columnId?: string
31
29
  position?: string
32
- ticketSubject?: string | null
30
+ /** The caller must share a department with the ticket (or transfer to all / be admin). */
31
+ ticketId?: string
32
+ }
33
+
34
+ /**
35
+ * Where the card goes. The target column is `columnId`, else the column of the neighbours, else
36
+ * the current one; `moveTo` puts it first/last of the target column. Moving into the `'final'`
37
+ * column closes the ticket, moving out of it reopens the ticket.
38
+ */
39
+ export type ReorderKanbanCardPayload = {
40
+ /** The card that ends up above. Must be in the same column as `afterCardId`. */
41
+ beforeCardId?: string
42
+ /** The card that ends up below. */
43
+ afterCardId?: string
44
+ columnId?: string
45
+ moveTo?: 'start' | 'end'
33
46
  }
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { KanbanColumnsApi } from './KanbanColumnsApi'
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('KanbanColumnsApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: KanbanColumnsApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new KanbanColumnsApi(client)
30
+ })
31
+
32
+ describe('reorder', () => {
33
+ it('calls client.patch with /kanban-columns/:id/reorder and body', async () => {
34
+ vi.mocked(client.patch).mockResolvedValue({ id: 'col1', position: 'N' })
35
+ const body = { beforeColumnId: 'col0', afterColumnId: 'col2' }
36
+ const result = await api.reorder('col1', body, headers)
37
+ expect(client.patch).toHaveBeenCalledWith('/kanban-columns/col1/reorder', body, headers)
38
+ expect(result).toEqual({ id: 'col1', position: 'N' })
39
+ })
40
+
41
+ it('propagates errors', async () => {
42
+ vi.mocked(client.patch).mockRejectedValue(new Error('Invalid reorder operation.'))
43
+ await expect(api.reorder('col1', {})).rejects.toThrow('Invalid reorder operation.')
44
+ })
45
+ })
46
+ })
@@ -1,6 +1,11 @@
1
1
  import type { ApiClient } from '../../core/ApiClient'
2
2
  import { BaseCrudApi } from '../../core/BaseCrudApi'
3
- import type { KanbanColumn, CreateKanbanColumnPayload, UpdateKanbanColumnPayload } from './types'
3
+ import type {
4
+ KanbanColumn,
5
+ CreateKanbanColumnPayload,
6
+ ReorderKanbanColumnPayload,
7
+ UpdateKanbanColumnPayload,
8
+ } from './types'
4
9
 
5
10
  export class KanbanColumnsApi extends BaseCrudApi<
6
11
  KanbanColumn,
@@ -10,4 +15,22 @@ export class KanbanColumnsApi extends BaseCrudApi<
10
15
  constructor(client: ApiClient) {
11
16
  super(client, '/kanban-columns')
12
17
  }
18
+
19
+ /**
20
+ * Moves a column between two neighbours of the same board. Both the column and a neighbour
21
+ * must be draggable; swapped neighbours (`before` after `after`) fail with 500. Resolves to the
22
+ * column with its new `position`.
23
+ * @permissions kanban.columns.update
24
+ */
25
+ reorder(
26
+ id: string,
27
+ body: ReorderKanbanColumnPayload,
28
+ headers?: Record<string, string>,
29
+ ): Promise<KanbanColumn> {
30
+ return this.client.patch<KanbanColumn, ReorderKanbanColumnPayload>(
31
+ `${this.basePath}/${id}/reorder`,
32
+ body,
33
+ headers,
34
+ )
35
+ }
13
36
  }
@@ -14,18 +14,22 @@ export type KanbanColumn = {
14
14
  accountId: string
15
15
  createdAt: string
16
16
  updatedAt: string
17
- deletedAt: string | null
18
17
  // relationships
19
- kanbanBoard?: KanbanBoard
18
+ board?: KanbanBoard | null
20
19
  cards?: KanbanCard[]
21
20
  }
22
21
 
23
- export type KanbanColumnRelationships = 'kanbanBoard' | 'cards'
22
+ export type KanbanColumnRelationships = 'cards'
24
23
 
25
24
  export type CreateKanbanColumnPayload = {
26
25
  name: string
27
26
  kanbanBoardId: string
27
+ /** A board has one `'initial'` and one `'final'` column (a second one fails). */
28
28
  role?: KanbanColumnRole
29
+ /**
30
+ * @deprecated Ignored by the backend: the position is computed (`'initial'` first, `'final'`
31
+ * last, other columns just before the final one). Use `reorder` to move a column.
32
+ */
29
33
  position?: string
30
34
  ticketWaitTimeMinutes?: number
31
35
  isDraggable?: boolean
@@ -38,3 +42,11 @@ export type UpdateKanbanColumnPayload = {
38
42
  ticketWaitTimeMinutes?: number | null
39
43
  isDraggable?: boolean
40
44
  }
45
+
46
+ /** Neighbours of the column after the move (omit both to move it to the middle, `'M'`). */
47
+ export type ReorderKanbanColumnPayload = {
48
+ /** The column that ends up on the left. */
49
+ beforeColumnId?: string
50
+ /** The column that ends up on the right. */
51
+ afterColumnId?: string
52
+ }
@@ -1,6 +1,9 @@
1
1
  import { expectError } from 'tsd'
2
2
  import type { ApiClient } from '../core/ApiClient'
3
+ import { CardsApi } from './cards/CardsApi'
3
4
  import { ContactsApi } from './contacts/ContactsApi'
5
+ import { KanbanColumnsApi } from './kanbanColumns/KanbanColumnsApi'
6
+ import { PipelineApi } from './pipeline/PipelineApi'
4
7
  import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
5
8
  import { RolesApi } from './roles/RolesApi'
6
9
  import { TagsApi } from './tags/TagsApi'
@@ -63,3 +66,30 @@ const tags = new TagsApi(client)
63
66
  tags.attachContacts('t-1', { where: { id: { $in: ['c-1', 'c-2'] } } })
64
67
  expectError(tags.attachContacts('t-1', {}))
65
68
  expectError(tags.detachContacts('t-1', { include: ['tags'] }))
69
+
70
+ // ── pipelines: moving the cards of a deleted status needs the target ─
71
+
72
+ const pipelines = new PipelineApi(client)
73
+ pipelines.deleteStatus('st-1', { action: 'move-to-another', newStatusId: 'st-2' })
74
+ pipelines.deleteStatus('st-1', { action: 'keep-without-status' })
75
+ expectError(pipelines.deleteStatus('st-1', { action: 'move-to-another' }))
76
+ expectError(pipelines.createAutomation({ pipelineId: 'p-1', type: 'email' }))
77
+ expectError(pipelines.deleteStage('s-1', { typeAction: 'archive' }))
78
+
79
+ // ── cards: getCards is not scoped by the backend and only answers the attributes asked ─
80
+
81
+ const cards = new CardsApi(client)
82
+ cards.getCards({
83
+ where: { accountId: 'a-1', pipelineId: 'p-1' },
84
+ attributes: ['id', 'title'],
85
+ order: [['contact.name', 'ASC']],
86
+ })
87
+ expectError(cards.getCards({ where: { pipelineId: 'p-1' }, attributes: ['id'] }))
88
+ expectError(cards.getCards({ where: { accountId: 'a-1' } }))
89
+ expectError(cards.saveComment({ comment: 'no card nor id' }))
90
+
91
+ // ── kanban columns: the backend computes the position ───────────────
92
+
93
+ const columns = new KanbanColumnsApi(client)
94
+ columns.reorder('col-1', { beforeColumnId: 'col-0', afterColumnId: 'col-2' })
95
+ expectError(columns.reorder('col-1', { position: 'M' }))
@@ -0,0 +1,193 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { PipelineApi } from './PipelineApi'
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('PipelineApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: PipelineApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new PipelineApi(client)
30
+ })
31
+
32
+ describe('stages', () => {
33
+ const stage = { name: 'Proposta', pipelineId: 'p1', position: 2 }
34
+
35
+ it('createStage calls client.post with /pipelines/stage and body', async () => {
36
+ vi.mocked(client.post).mockResolvedValue({ id: 's1', ...stage })
37
+ const result = await api.createStage(stage)
38
+ expect(client.post).toHaveBeenCalledWith('/pipelines/stage', stage, undefined)
39
+ expect(result).toEqual({ id: 's1', ...stage })
40
+ })
41
+
42
+ it('createStage resolves to the refusal code the backend answers with 200', async () => {
43
+ vi.mocked(client.post).mockResolvedValue('STAGES_LIMIT')
44
+ await expect(api.createStage(stage)).resolves.toBe('STAGES_LIMIT')
45
+ })
46
+
47
+ it('updateStage calls client.put with /pipelines/stage/:id and body', async () => {
48
+ const body = { ...stage, id: 's1', statuses: [{ id: 'st1', name: 'Novo', position: 1 }] }
49
+ await api.updateStage('s1', body, headers)
50
+ expect(client.put).toHaveBeenCalledWith('/pipelines/stage/s1', body, headers)
51
+ })
52
+
53
+ it('deleteStage calls client.request with DELETE and the destination of the cards', async () => {
54
+ vi.mocked(client.request).mockResolvedValue('MESSAGE_STAGE_DELETED')
55
+ const body = { typeAction: 'move' as const, idNewStage: 's2', user: { id: 'u1' } }
56
+ const result = await api.deleteStage('s1', body)
57
+ expect(client.request).toHaveBeenCalledWith('DELETE', '/pipelines/stage/s1', body, undefined)
58
+ expect(result).toBe('MESSAGE_STAGE_DELETED')
59
+ })
60
+
61
+ it('deleteStage sends an empty body by default', async () => {
62
+ await api.deleteStage('s1')
63
+ expect(client.request).toHaveBeenCalledWith('DELETE', '/pipelines/stage/s1', {}, undefined)
64
+ })
65
+
66
+ it('getStageTotals calls client.post with /pipelines/stage-totals/:id and the filters', async () => {
67
+ const filters = { nameTitle: 'Acme', createdFrom: '01/09/2026' }
68
+ await api.getStageTotals('s1', filters)
69
+ expect(client.post).toHaveBeenCalledWith('/pipelines/stage-totals/s1', filters, undefined)
70
+ })
71
+
72
+ it('getStageTotals sends no filter by default', async () => {
73
+ await api.getStageTotals('s1')
74
+ expect(client.post).toHaveBeenCalledWith('/pipelines/stage-totals/s1', {}, undefined)
75
+ })
76
+ })
77
+
78
+ describe('statuses', () => {
79
+ it('getStatuses calls client.get with /pipelines/statuses/:pipelineId', async () => {
80
+ vi.mocked(client.get).mockResolvedValue([{ id: 'st1', name: 'Novo' }])
81
+ const result = await api.getStatuses('p1', headers)
82
+ expect(client.get).toHaveBeenCalledWith('/pipelines/statuses/p1', headers)
83
+ expect(result).toEqual([{ id: 'st1', name: 'Novo' }])
84
+ })
85
+
86
+ it('getStatus calls client.get with /pipelines/status/:id', async () => {
87
+ await api.getStatus('st1')
88
+ expect(client.get).toHaveBeenCalledWith('/pipelines/status/st1', undefined)
89
+ })
90
+
91
+ it('deleteStatus calls client.post with /pipelines/delete-status/:id and the action', async () => {
92
+ vi.mocked(client.post).mockResolvedValue('MESSAGE_STATUS_DELETED')
93
+ const body = { action: 'move-to-another' as const, newStatusId: 'st2' }
94
+ const result = await api.deleteStatus('st1', body)
95
+ expect(client.post).toHaveBeenCalledWith('/pipelines/delete-status/st1', body, undefined)
96
+ expect(result).toBe('MESSAGE_STATUS_DELETED')
97
+ })
98
+
99
+ it('renameStatus posts only newStatusName to the delete-status route', async () => {
100
+ vi.mocked(client.post).mockResolvedValue('MESSAGE_STATUS_RENAMED')
101
+ const result = await api.renameStatus('st1', 'Qualificado', headers)
102
+ expect(client.post).toHaveBeenCalledWith(
103
+ '/pipelines/delete-status/st1',
104
+ { newStatusName: 'Qualificado' },
105
+ headers,
106
+ )
107
+ expect(result).toBe('MESSAGE_STATUS_RENAMED')
108
+ })
109
+ })
110
+
111
+ describe('automations', () => {
112
+ const body = {
113
+ pipelineId: 'p1',
114
+ type: 'notification' as const,
115
+ config: { notify: 'high' as const },
116
+ }
117
+
118
+ it('createAutomation calls client.post with /pipelines/automations and body', async () => {
119
+ await api.createAutomation(body)
120
+ expect(client.post).toHaveBeenCalledWith('/pipelines/automations', body, undefined)
121
+ })
122
+
123
+ it('updateAutomation calls client.put with /pipelines/automations/:id and body', async () => {
124
+ await api.updateAutomation('a1', body, headers)
125
+ expect(client.put).toHaveBeenCalledWith('/pipelines/automations/a1', body, headers)
126
+ })
127
+
128
+ it('deleteAutomation calls client.delete with /pipelines/automations/:id', async () => {
129
+ await api.deleteAutomation('a1')
130
+ expect(client.delete).toHaveBeenCalledWith('/pipelines/automations/a1', undefined)
131
+ })
132
+
133
+ it('getAutomation calls client.get with /pipelines/automations/:pipelineId', async () => {
134
+ vi.mocked(client.get).mockResolvedValue([])
135
+ const result = await api.getAutomation('p1')
136
+ expect(client.get).toHaveBeenCalledWith('/pipelines/automations/p1', undefined)
137
+ expect(result).toEqual([])
138
+ })
139
+ })
140
+
141
+ describe('pipeline actions', () => {
142
+ it('duplicate calls client.post with /pipelines/duplicate/:pipelineId and no body', async () => {
143
+ await api.duplicate('p1', headers)
144
+ expect(client.post).toHaveBeenCalledWith('/pipelines/duplicate/p1', undefined, headers)
145
+ })
146
+
147
+ it('archive posts archive: true to /pipelines/:id/archive', async () => {
148
+ await api.archive('p1')
149
+ expect(client.post).toHaveBeenCalledWith(
150
+ '/pipelines/p1/archive',
151
+ { archive: true },
152
+ undefined,
153
+ )
154
+ })
155
+
156
+ it('unarchive posts archive: false to /pipelines/:id/archive', async () => {
157
+ await api.unarchive('p1')
158
+ expect(client.post).toHaveBeenCalledWith(
159
+ '/pipelines/p1/archive',
160
+ { archive: false },
161
+ undefined,
162
+ )
163
+ })
164
+
165
+ it('exportCsv calls client.post with /pipelines/:id/export and resolves to the CSV text', async () => {
166
+ vi.mocked(client.post).mockResolvedValue('Funil;\n')
167
+ const body = { createdAtInit: '2026-09-01 00:00:00', createdAtFinal: '2026-09-30 23:59:59' }
168
+ const result = await api.exportCsv('p1', body)
169
+ expect(client.post).toHaveBeenCalledWith('/pipelines/p1/export', body, undefined)
170
+ expect(result).toBe('Funil;\n')
171
+ })
172
+
173
+ it('getManyWithTotals serializes the query on /pipelines/withTotals', async () => {
174
+ await api.getManyWithTotals({ archive: true })
175
+ const expected = `/pipelines/withTotals?query=${encodeURIComponent(JSON.stringify({ archive: true }))}`
176
+ expect(client.get).toHaveBeenCalledWith(expected, undefined)
177
+ })
178
+
179
+ it('getManyWithTotals sends no query string without a query', async () => {
180
+ await api.getManyWithTotals()
181
+ expect(client.get).toHaveBeenCalledWith('/pipelines/withTotals', undefined)
182
+ })
183
+ })
184
+
185
+ describe('error propagation', () => {
186
+ it('propagates errors from createStage', async () => {
187
+ vi.mocked(client.post).mockRejectedValue(new Error('Validation failed'))
188
+ await expect(api.createStage({ name: 'x', pipelineId: 'p1', position: 1 })).rejects.toThrow(
189
+ 'Validation failed',
190
+ )
191
+ })
192
+ })
193
+ })