@ikatec/digisac-api-sdk 4.4.1 → 4.5.1

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 (55) 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/permissions/types.d.ts +1 -1
  28. package/dist/apis/permissions/types.d.ts.map +1 -1
  29. package/dist/apis/pipeline/PipelineApi.cjs +133 -0
  30. package/dist/apis/pipeline/PipelineApi.cjs.map +1 -1
  31. package/dist/apis/pipeline/PipelineApi.d.ts +102 -1
  32. package/dist/apis/pipeline/PipelineApi.d.ts.map +1 -1
  33. package/dist/apis/pipeline/PipelineApi.mjs +133 -0
  34. package/dist/apis/pipeline/PipelineApi.mjs.map +1 -1
  35. package/dist/apis/pipeline/types.d.ts +164 -0
  36. package/dist/apis/pipeline/types.d.ts.map +1 -1
  37. package/dist/apis/tickets/types.d.ts +37 -1
  38. package/dist/apis/tickets/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/apis/cards/CardsApi.test.ts +128 -0
  41. package/src/apis/cards/CardsApi.ts +124 -1
  42. package/src/apis/cards/types.ts +81 -0
  43. package/src/apis/kanbanBoards/types.ts +36 -4
  44. package/src/apis/kanbanCards/KanbanCardsApi.test.ts +46 -0
  45. package/src/apis/kanbanCards/KanbanCardsApi.ts +23 -1
  46. package/src/apis/kanbanCards/types.ts +18 -5
  47. package/src/apis/kanbanColumns/KanbanColumnsApi.test.ts +46 -0
  48. package/src/apis/kanbanColumns/KanbanColumnsApi.ts +24 -1
  49. package/src/apis/kanbanColumns/types.ts +15 -3
  50. package/src/apis/payloads.test-d.ts +53 -1
  51. package/src/apis/permissions/types.ts +10 -4
  52. package/src/apis/pipeline/PipelineApi.test.ts +193 -0
  53. package/src/apis/pipeline/PipelineApi.ts +274 -1
  54. package/src/apis/pipeline/types.ts +162 -0
  55. package/src/apis/tickets/types.ts +40 -1
@@ -1,14 +1,32 @@
1
1
  import type { Department } from '../departments/types'
2
2
  import type { KanbanColumn } from '../kanbanColumns/types'
3
3
 
4
+ export type KanbanBoardVisibleTickets = 'mine' | 'all' | 'mine-and-not-assigned'
5
+
6
+ export type KanbanBoardClosedMaxTimeUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months'
7
+
8
+ /** Card fields shown on the board. */
9
+ export type KanbanBoardSettings = {
10
+ lastMessage: boolean
11
+ departmentAndUser: boolean
12
+ tags: boolean
13
+ createdAtDate: boolean
14
+ newInteractions: boolean
15
+ timeoutAlert: boolean
16
+ }
17
+
4
18
  export type KanbanBoard = {
5
19
  id: string
6
20
  name: string
7
- timeoutAlertEnabled: boolean
8
21
  accountId: string
22
+ visibleTickets: KanbanBoardVisibleTickets | null
23
+ closedMaxTime: number | null
24
+ closedMaxTimeUnit: KanbanBoardClosedMaxTimeUnit | null
25
+ /** `'initializing'` while the cards of the open tickets are being created. */
26
+ status: 'initializing' | 'ready'
27
+ settings: KanbanBoardSettings
9
28
  createdAt: string
10
29
  updatedAt: string
11
- deletedAt: string | null
12
30
  // relationships
13
31
  departments?: Department[]
14
32
  columns?: KanbanColumn[]
@@ -18,12 +36,26 @@ export type KanbanBoardRelationships = 'departments' | 'columns'
18
36
 
19
37
  export type CreateKanbanBoardPayload = {
20
38
  name: string
21
- timeoutAlertEnabled?: boolean
22
39
  /** Department IDs to link to the board. Required by the backend on creation. */
23
40
  departmentIds: string[]
41
+ visibleTickets?: KanbanBoardVisibleTickets
42
+ /** Greater than 0. */
43
+ closedMaxTime?: number
44
+ closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
45
+ settings?: Partial<KanbanBoardSettings>
24
46
  }
25
47
 
26
48
  export type UpdateKanbanBoardPayload = {
27
49
  name?: string
28
- timeoutAlertEnabled?: boolean
50
+ /**
51
+ * Replaces the departments of the board (at least one): removing a department deletes the
52
+ * cards of its tickets, adding one creates cards for its open tickets.
53
+ */
54
+ departmentIds?: string[]
55
+ visibleTickets?: KanbanBoardVisibleTickets
56
+ /** Greater than 0. */
57
+ closedMaxTime?: number
58
+ closedMaxTimeUnit?: KanbanBoardClosedMaxTimeUnit
59
+ /** Merged into the current settings — send only the keys to change. */
60
+ settings?: Partial<KanbanBoardSettings>
29
61
  }
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import type { ApiClient } from '../../core/ApiClient'
3
+ import { KanbanCardsApi } from './KanbanCardsApi'
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('KanbanCardsApi', () => {
24
+ let client: ReturnType<typeof createMockClient>
25
+ let api: KanbanCardsApi
26
+
27
+ beforeEach(() => {
28
+ client = createMockClient()
29
+ api = new KanbanCardsApi(client)
30
+ })
31
+
32
+ describe('reorder', () => {
33
+ it('calls client.patch with /kanban-cards/:id/reorder and body', async () => {
34
+ vi.mocked(client.patch).mockResolvedValue({ id: 'kc1', position: 'N' })
35
+ const body = { columnId: 'col2', moveTo: 'end' as const }
36
+ const result = await api.reorder('kc1', body, headers)
37
+ expect(client.patch).toHaveBeenCalledWith('/kanban-cards/kc1/reorder', body, headers)
38
+ expect(result).toEqual({ id: 'kc1', 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('kc1', {})).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 { 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,9 +1,15 @@
1
- import { expectError } from 'tsd'
1
+ import { expectAssignable, expectError, expectNotAssignable, expectType } 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'
7
+ import type { PermissionName } from './permissions/types'
4
8
  import { PersonalAccessTokensApi } from './personalAccessTokens/PersonalAccessTokensApi'
5
9
  import { RolesApi } from './roles/RolesApi'
6
10
  import { TagsApi } from './tags/TagsApi'
11
+ import { TicketsApi } from './tickets/TicketsApi'
12
+ import type { Ticket, TicketTransfer } from './tickets/types'
7
13
  import { TimetableApi } from './timetable/TimetableApi'
8
14
  import { UsersApi } from './users/UsersApi'
9
15
 
@@ -63,3 +69,49 @@ const tags = new TagsApi(client)
63
69
  tags.attachContacts('t-1', { where: { id: { $in: ['c-1', 'c-2'] } } })
64
70
  expectError(tags.attachContacts('t-1', {}))
65
71
  expectError(tags.detachContacts('t-1', { include: ['tags'] }))
72
+
73
+ // ── pipelines: moving the cards of a deleted status needs the target ─
74
+
75
+ const pipelines = new PipelineApi(client)
76
+ pipelines.deleteStatus('st-1', { action: 'move-to-another', newStatusId: 'st-2' })
77
+ pipelines.deleteStatus('st-1', { action: 'keep-without-status' })
78
+ expectError(pipelines.deleteStatus('st-1', { action: 'move-to-another' }))
79
+ expectError(pipelines.createAutomation({ pipelineId: 'p-1', type: 'email' }))
80
+ expectError(pipelines.deleteStage('s-1', { typeAction: 'archive' }))
81
+
82
+ // ── cards: getCards is not scoped by the backend and only answers the attributes asked ─
83
+
84
+ const cards = new CardsApi(client)
85
+ cards.getCards({
86
+ where: { accountId: 'a-1', pipelineId: 'p-1' },
87
+ attributes: ['id', 'title'],
88
+ order: [['contact.name', 'ASC']],
89
+ })
90
+ expectError(cards.getCards({ where: { pipelineId: 'p-1' }, attributes: ['id'] }))
91
+ expectError(cards.getCards({ where: { accountId: 'a-1' } }))
92
+ expectError(cards.saveComment({ comment: 'no card nor id' }))
93
+
94
+ // ── kanban columns: the backend computes the position ───────────────
95
+
96
+ const columns = new KanbanColumnsApi(client)
97
+ columns.reorder('col-1', { beforeColumnId: 'col-0', afterColumnId: 'col-2' })
98
+ expectError(columns.reorder('col-1', { position: 'M' }))
99
+
100
+ // ── permissions: the kanban permissions were split by the migration 20260209144630 ──
101
+
102
+ expectAssignable<PermissionName>('kanban.boards.view')
103
+ expectAssignable<PermissionName>('kanban.cards.move')
104
+ expectAssignable<PermissionName>('users.export')
105
+ expectNotAssignable<PermissionName>('kanban.view')
106
+ expectNotAssignable<PermissionName>('kanban.update')
107
+
108
+ // ── tickets: the history of the ticket comes in the ticketTransfers include ──
109
+
110
+ const tickets = new TicketsApi(client)
111
+ tickets.getById('t-1', { include: ['ticketTransfers'] })
112
+ tickets.getById('t-1', { include: [{ model: 'ticketTransfers', include: ['byUser'] }] })
113
+ declare const ticket: Ticket
114
+ expectType<TicketTransfer[] | undefined>(ticket.ticketTransfers)
115
+ declare const transfer: TicketTransfer
116
+ expectType<string | null>(transfer.byUserId)
117
+ expectType<'opened' | 'transferred' | 'closed' | null>(transfer.action)
@@ -47,6 +47,7 @@ export type PermissionName =
47
47
  | 'users.create'
48
48
  | 'users.update'
49
49
  | 'users.archive'
50
+ | 'users.export'
50
51
  | 'roles.view'
51
52
  | 'roles.create'
52
53
  | 'roles.update'
@@ -205,10 +206,15 @@ export type PermissionName =
205
206
  | 'knowledgeBase.create'
206
207
  | 'knowledgeBase.update'
207
208
  | 'knowledgeBase.destroy'
208
- | 'kanban.view'
209
- | 'kanban.create'
210
- | 'kanban.update'
211
- | 'kanban.destroy'
209
+ // The migration 20260209144630 replaced kanban.view/create/update/destroy with the permissions below.
210
+ | 'kanban.boards.view'
211
+ | 'kanban.boards.create'
212
+ | 'kanban.boards.update'
213
+ | 'kanban.boards.destroy'
214
+ | 'kanban.columns.create'
215
+ | 'kanban.columns.update'
216
+ | 'kanban.columns.destroy'
217
+ | 'kanban.cards.move'
212
218
 
213
219
  /**
214
220
  * A permission, as `GET /permissions` answers it (the transformer only picks these fields).
@@ -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
+ })