@ikatec/digisac-api-sdk 1.4.0 → 2.1.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 (77) hide show
  1. package/dist/_chunks/{chunk-DortyG5C.cjs → chunk-Dk0Otj32.cjs} +19 -0
  2. package/dist/_chunks/scopes-CppNX42t.mjs +29 -0
  3. package/dist/_chunks/scopes-CppNX42t.mjs.map +1 -0
  4. package/dist/apis/authHistory/AuthHistoryApi.cjs +1 -1
  5. package/dist/apis/campaigns/CampaignsApi.cjs +12 -0
  6. package/dist/apis/campaigns/CampaignsApi.cjs.map +1 -1
  7. package/dist/apis/campaigns/CampaignsApi.d.ts +2 -2
  8. package/dist/apis/campaigns/CampaignsApi.d.ts.map +1 -1
  9. package/dist/apis/campaigns/CampaignsApi.mjs +12 -0
  10. package/dist/apis/campaigns/CampaignsApi.mjs.map +1 -1
  11. package/dist/apis/campaigns/index.cjs +8 -1
  12. package/dist/apis/campaigns/index.d.ts +1 -0
  13. package/dist/apis/campaigns/index.d.ts.map +1 -1
  14. package/dist/apis/campaigns/index.mjs +2 -1
  15. package/dist/apis/campaigns/scopes/getManyViewScope.cjs +51 -0
  16. package/dist/apis/campaigns/scopes/getManyViewScope.cjs.map +1 -0
  17. package/dist/apis/campaigns/scopes/getManyViewScope.d.ts +21 -0
  18. package/dist/apis/campaigns/scopes/getManyViewScope.d.ts.map +1 -0
  19. package/dist/apis/campaigns/scopes/getManyViewScope.mjs +49 -0
  20. package/dist/apis/campaigns/scopes/getManyViewScope.mjs.map +1 -0
  21. package/dist/apis/campaigns/scopes/getOneViewScope.cjs +27 -0
  22. package/dist/apis/campaigns/scopes/getOneViewScope.cjs.map +1 -0
  23. package/dist/apis/campaigns/scopes/getOneViewScope.d.ts +5 -0
  24. package/dist/apis/campaigns/scopes/getOneViewScope.d.ts.map +1 -0
  25. package/dist/apis/campaigns/scopes/getOneViewScope.mjs +25 -0
  26. package/dist/apis/campaigns/scopes/getOneViewScope.mjs.map +1 -0
  27. package/dist/apis/campaigns/scopes/index.cjs +21 -0
  28. package/dist/apis/campaigns/scopes/index.cjs.map +1 -0
  29. package/dist/apis/campaigns/scopes/index.d.ts +3 -0
  30. package/dist/apis/campaigns/scopes/index.d.ts.map +1 -0
  31. package/dist/apis/campaigns/scopes/index.mjs +4 -0
  32. package/dist/apis/campaigns/types.d.ts +32 -16
  33. package/dist/apis/campaigns/types.d.ts.map +1 -1
  34. package/dist/apis/contacts/ContactsApi.cjs +1 -1
  35. package/dist/apis/files/types.d.ts +1 -0
  36. package/dist/apis/files/types.d.ts.map +1 -1
  37. package/dist/apis/index.cjs +8 -1
  38. package/dist/apis/index.mjs +2 -1
  39. package/dist/apis/messages/MessagesApi.cjs +1 -1
  40. package/dist/apis/messages/types.d.ts +2 -1
  41. package/dist/apis/messages/types.d.ts.map +1 -1
  42. package/dist/apis/serviceAccessManagement/ServiceAccessManagementApi.cjs +1 -1
  43. package/dist/apis/stickerUsers/StickerUsersApi.cjs +1 -1
  44. package/dist/apis/terms/TermsApi.cjs +1 -1
  45. package/dist/core/BaseCrudApi.cjs +9 -3
  46. package/dist/core/BaseCrudApi.cjs.map +1 -1
  47. package/dist/core/BaseCrudApi.d.ts +5 -5
  48. package/dist/core/BaseCrudApi.d.ts.map +1 -1
  49. package/dist/core/BaseCrudApi.mjs +8 -2
  50. package/dist/core/BaseCrudApi.mjs.map +1 -1
  51. package/dist/core/index.d.ts +1 -1
  52. package/dist/core/index.d.ts.map +1 -1
  53. package/dist/core/types.d.ts +3 -1
  54. package/dist/core/types.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/apis/authHistory/AuthHistoryApi.test.ts +110 -0
  57. package/src/apis/campaigns/CampaignsApi.test.ts +344 -0
  58. package/src/apis/campaigns/CampaignsApi.ts +14 -2
  59. package/src/apis/campaigns/index.ts +1 -0
  60. package/src/apis/campaigns/scopes/getManyViewScope.ts +79 -0
  61. package/src/apis/campaigns/scopes/getOneViewScope.ts +21 -0
  62. package/src/apis/campaigns/scopes/index.ts +2 -0
  63. package/src/apis/campaigns/types.ts +28 -16
  64. package/src/apis/clientFeedback/ClientFeedbackApi.test.ts +76 -0
  65. package/src/apis/contacts/ContactsApi.test.ts +364 -0
  66. package/src/apis/files/types.ts +1 -0
  67. package/src/apis/me/MeApi.test.ts +372 -0
  68. package/src/apis/messages/MessagesApi.test.ts +460 -0
  69. package/src/apis/messages/types.ts +2 -1
  70. package/src/apis/serviceAccessManagement/ServiceAccessManagementApi.test.ts +89 -0
  71. package/src/apis/stickerUsers/StickerUsersApi.test.ts +84 -0
  72. package/src/apis/terms/TermsApi.test.ts +196 -0
  73. package/src/apis/transcripts/TranscriptsApi.test.ts +63 -0
  74. package/src/core/BaseCrudApi.test.ts +1 -1
  75. package/src/core/BaseCrudApi.ts +16 -5
  76. package/src/core/index.ts +1 -1
  77. package/src/core/types.ts +8 -1
@@ -0,0 +1,344 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { CampaignsApi } from './CampaignsApi'
3
+ import type { ApiClient } from '../../core/ApiClient'
4
+ import type {
5
+ Campaign,
6
+ CreateCampaignPayload,
7
+ CampaignStats,
8
+ SetIntervalPayload,
9
+ AutoPauseModePayload,
10
+ ExportCampaignResultPayload,
11
+ ExportTemplateCsvPayload,
12
+ HsmLimit,
13
+ } from './types'
14
+ import type { Paginated } from '../../core/types'
15
+
16
+ // ── Mock client factory ──────────────────────────────────────────────
17
+
18
+ function createMockClient(): ApiClient {
19
+ return {
20
+ setAccessToken: vi.fn().mockReturnThis(),
21
+ request: vi.fn(),
22
+ get: vi.fn(),
23
+ post: vi.fn(),
24
+ put: vi.fn(),
25
+ patch: vi.fn(),
26
+ delete: vi.fn(),
27
+ }
28
+ }
29
+
30
+ // ── Fixtures ─────────────────────────────────────────────────────────
31
+
32
+ const campaign: Campaign = {
33
+ id: 'cmp-1',
34
+ title: 'Test Campaign',
35
+ status: 'ready',
36
+ config: { minInterval: 1000, maxInterval: 5000 },
37
+ sendsAt: null,
38
+ isScheduled: false,
39
+ startedAt: null,
40
+ finishedAt: null,
41
+ totalMessagesCount: null,
42
+ sentMessagesCount: null,
43
+ totalContacts: null,
44
+ totalContactsImported: null,
45
+ totalValidContacts: null,
46
+ sentContacts: null,
47
+ viewedContacts: null,
48
+ contactCount: null,
49
+ mustOpenTicket: false,
50
+ marketingIntegration: false,
51
+ accountId: 'acc-1',
52
+ serviceId: 'svc-1',
53
+ defaultDepartmentId: null,
54
+ defaultUserId: null,
55
+ createdById: null,
56
+ sentById: null,
57
+ createdAt: '2024-01-01T00:00:00Z',
58
+ updatedAt: '2024-01-01T00:00:00Z',
59
+ deletedAt: null,
60
+ }
61
+
62
+ const paginated: Paginated<Campaign> = {
63
+ data: [campaign],
64
+ total: 1,
65
+ limit: 10,
66
+ skip: 0,
67
+ currentPage: 1,
68
+ lastPage: 1,
69
+ from: 1,
70
+ to: 1,
71
+ }
72
+
73
+ // ── Tests ────────────────────────────────────────────────────────────
74
+
75
+ describe('CampaignsApi', () => {
76
+ let client: ReturnType<typeof createMockClient>
77
+ let api: CampaignsApi
78
+
79
+ beforeEach(() => {
80
+ client = createMockClient()
81
+ api = new CampaignsApi(client)
82
+ })
83
+
84
+ // ── getMany ──────────────────────────────────────────────────────
85
+
86
+ describe('getMany', () => {
87
+ it('calls client.get with /campaigns when no query is provided', async () => {
88
+ vi.mocked(client.get).mockResolvedValue(paginated)
89
+ const result = await api.getMany()
90
+ expect(client.get).toHaveBeenCalledWith('/campaigns', undefined)
91
+ expect(result).toEqual(paginated)
92
+ })
93
+
94
+ it('serializes query parameters', async () => {
95
+ vi.mocked(client.get).mockResolvedValue(paginated)
96
+ await api.getMany({ page: 2, perPage: 5 })
97
+ const url = vi.mocked(client.get).mock.calls[0][0] as string
98
+ expect(url).toContain('/campaigns?')
99
+ expect(url).toContain('page=2')
100
+ })
101
+
102
+ it('forwards custom headers', async () => {
103
+ vi.mocked(client.get).mockResolvedValue(paginated)
104
+ const headers = { 'X-Custom': 'value' }
105
+ await api.getMany(undefined, headers)
106
+ expect(client.get).toHaveBeenCalledWith('/campaigns', headers)
107
+ })
108
+ })
109
+
110
+ // ── getById ──────────────────────────────────────────────────────
111
+
112
+ describe('getById', () => {
113
+ it('calls client.get with /campaigns/:id', async () => {
114
+ vi.mocked(client.get).mockResolvedValue(campaign)
115
+ const result = await api.getById('cmp-1')
116
+ expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1', undefined)
117
+ expect(result).toEqual(campaign)
118
+ })
119
+
120
+ it('forwards custom headers', async () => {
121
+ vi.mocked(client.get).mockResolvedValue(campaign)
122
+ const headers = { 'X-Tenant': 'abc' }
123
+ await api.getById('cmp-1', undefined, headers)
124
+ expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1', headers)
125
+ })
126
+ })
127
+
128
+ // ── create ───────────────────────────────────────────────────────
129
+
130
+ describe('create', () => {
131
+ it('calls client.post with /campaigns and body', async () => {
132
+ vi.mocked(client.post).mockResolvedValue(campaign)
133
+ const body: CreateCampaignPayload = { title: 'New Campaign', serviceId: 'svc-1' }
134
+ const result = await api.create(body)
135
+ expect(client.post).toHaveBeenCalledWith('/campaigns', body, undefined)
136
+ expect(result).toEqual(campaign)
137
+ })
138
+
139
+ it('forwards custom headers', async () => {
140
+ vi.mocked(client.post).mockResolvedValue(campaign)
141
+ const headers = { 'X-Req': 'r-1' }
142
+ await api.create({ title: 'x', serviceId: 'svc-1' }, headers)
143
+ expect(client.post).toHaveBeenCalledWith(
144
+ '/campaigns',
145
+ { title: 'x', serviceId: 'svc-1' },
146
+ headers,
147
+ )
148
+ })
149
+ })
150
+
151
+ // ── updateById ───────────────────────────────────────────────────
152
+
153
+ describe('updateById', () => {
154
+ it('calls client.put with /campaigns/:id and body', async () => {
155
+ vi.mocked(client.put).mockResolvedValue(campaign)
156
+ const result = await api.updateById('cmp-1', { title: 'Updated' })
157
+ expect(client.put).toHaveBeenCalledWith('/campaigns/cmp-1', { title: 'Updated' }, undefined)
158
+ expect(result).toEqual(campaign)
159
+ })
160
+
161
+ it('forwards custom headers', async () => {
162
+ vi.mocked(client.put).mockResolvedValue(campaign)
163
+ const headers = { 'If-Match': 'etag' }
164
+ await api.updateById('cmp-1', { title: 'x' }, headers)
165
+ expect(client.put).toHaveBeenCalledWith('/campaigns/cmp-1', { title: 'x' }, headers)
166
+ })
167
+ })
168
+
169
+ // ── deleteById ───────────────────────────────────────────────────
170
+
171
+ describe('deleteById', () => {
172
+ it('calls client.delete with /campaigns/:id', async () => {
173
+ vi.mocked(client.delete).mockResolvedValue(campaign)
174
+ const result = await api.deleteById('cmp-1')
175
+ expect(client.delete).toHaveBeenCalledWith('/campaigns/cmp-1', undefined)
176
+ expect(result).toEqual(campaign)
177
+ })
178
+
179
+ it('forwards custom headers', async () => {
180
+ vi.mocked(client.delete).mockResolvedValue(campaign)
181
+ const headers = { 'X-Reason': 'cleanup' }
182
+ await api.deleteById('cmp-1', headers)
183
+ expect(client.delete).toHaveBeenCalledWith('/campaigns/cmp-1', headers)
184
+ })
185
+ })
186
+
187
+ // ── sendById ─────────────────────────────────────────────────────
188
+
189
+ describe('sendById', () => {
190
+ it('calls client.post with /campaigns/:id/send', async () => {
191
+ vi.mocked(client.post).mockResolvedValue(campaign)
192
+ const result = await api.sendById('cmp-1')
193
+ expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/send')
194
+ expect(result).toEqual(campaign)
195
+ })
196
+ })
197
+
198
+ // ── pauseById ────────────────────────────────────────────────────
199
+
200
+ describe('pauseById', () => {
201
+ it('calls client.post with /campaigns/:id/pause', async () => {
202
+ vi.mocked(client.post).mockResolvedValue(campaign)
203
+ const result = await api.pauseById('cmp-1')
204
+ expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/pause')
205
+ expect(result).toEqual(campaign)
206
+ })
207
+ })
208
+
209
+ // ── resumeById ───────────────────────────────────────────────────
210
+
211
+ describe('resumeById', () => {
212
+ it('calls client.post with /campaigns/:id/resume', async () => {
213
+ vi.mocked(client.post).mockResolvedValue(campaign)
214
+ const result = await api.resumeById('cmp-1')
215
+ expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/resume')
216
+ expect(result).toEqual(campaign)
217
+ })
218
+ })
219
+
220
+ // ── duplicateById ────────────────────────────────────────────────
221
+
222
+ describe('duplicateById', () => {
223
+ it('calls client.post with /campaigns/:id/duplicate', async () => {
224
+ vi.mocked(client.post).mockResolvedValue(campaign)
225
+ const result = await api.duplicateById('cmp-1')
226
+ expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/duplicate')
227
+ expect(result).toEqual(campaign)
228
+ })
229
+ })
230
+
231
+ // ── getStatsById ─────────────────────────────────────────────────
232
+
233
+ describe('getStatsById', () => {
234
+ it('calls client.get with /campaigns/:id/stats', async () => {
235
+ const stats: CampaignStats = {
236
+ campaignInfo: {
237
+ totalValidContacts: 100,
238
+ totalContacts: 110,
239
+ totalContactsImported: 120,
240
+ firedSuccessCount: 90,
241
+ firedFailedCount: 10,
242
+ totalMessagesCount: 100,
243
+ totalFiredCount: 100,
244
+ firedBlockedByMessageRuleCount: 0,
245
+ },
246
+ messageInfo: { error: 5, received: 85, viewed: 70, answered: 30 },
247
+ }
248
+ vi.mocked(client.get).mockResolvedValue(stats)
249
+ const result = await api.getStatsById('cmp-1')
250
+ expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1/stats')
251
+ expect(result).toEqual(stats)
252
+ })
253
+ })
254
+
255
+ // ── setIntervalById ──────────────────────────────────────────────
256
+
257
+ describe('setIntervalById', () => {
258
+ it('calls client.post with /campaigns/:id/interval and body', async () => {
259
+ vi.mocked(client.post).mockResolvedValue(campaign)
260
+ const data: SetIntervalPayload = { minInterval: '1000', maxInterval: '5000' }
261
+ const result = await api.setIntervalById('cmp-1', data)
262
+ expect(client.post).toHaveBeenCalledWith('/campaigns/cmp-1/interval', data)
263
+ expect(result).toEqual(campaign)
264
+ })
265
+ })
266
+
267
+ // ── getContactCount ──────────────────────────────────────────────
268
+
269
+ describe('getContactCount', () => {
270
+ it('calls client.get with /campaigns/:id/contactCount', async () => {
271
+ vi.mocked(client.get).mockResolvedValue(42)
272
+ const result = await api.getContactCount('cmp-1')
273
+ expect(client.get).toHaveBeenCalledWith('/campaigns/cmp-1/contactCount')
274
+ expect(result).toBe(42)
275
+ })
276
+ })
277
+
278
+ // ── setAutoPauseMode ─────────────────────────────────────────────
279
+
280
+ describe('setAutoPauseMode', () => {
281
+ it('calls client.put with /campaigns/autoPauseMode and body', async () => {
282
+ vi.mocked(client.put).mockResolvedValue({ ok: true })
283
+ const data: AutoPauseModePayload = { mode: 'auto' }
284
+ const result = await api.setAutoPauseMode(data)
285
+ expect(client.put).toHaveBeenCalledWith('/campaigns/autoPauseMode', data)
286
+ expect(result).toEqual({ ok: true })
287
+ })
288
+ })
289
+
290
+ // ── exportCampaignResult ─────────────────────────────────────────
291
+
292
+ describe('exportCampaignResult', () => {
293
+ it('calls client.post with /campaigns/export/csv and body', async () => {
294
+ vi.mocked(client.post).mockResolvedValue({ ok: true })
295
+ const data: ExportCampaignResultPayload = { campaignId: 'cmp-1' }
296
+ const result = await api.exportCampaignResult(data)
297
+ expect(client.post).toHaveBeenCalledWith('/campaigns/export/csv', data)
298
+ expect(result).toEqual({ ok: true })
299
+ })
300
+ })
301
+
302
+ // ── exportTemplateCsv ────────────────────────────────────────────
303
+
304
+ describe('exportTemplateCsv', () => {
305
+ it('calls client.post with /campaigns/exportTemplate and body', async () => {
306
+ vi.mocked(client.post).mockResolvedValue({ ok: true })
307
+ const data: ExportTemplateCsvPayload = { hsmId: 'hsm-1' }
308
+ const result = await api.exportTemplateCsv(data)
309
+ expect(client.post).toHaveBeenCalledWith('/campaigns/exportTemplate', data)
310
+ expect(result).toEqual({ ok: true })
311
+ })
312
+ })
313
+
314
+ // ── getHsmLimit ──────────────────────────────────────────────────
315
+
316
+ describe('getHsmLimit', () => {
317
+ it('calls client.get with /whatsapp-business-templates/:accountId/hsmLimit', async () => {
318
+ const limit: HsmLimit = { hsmLimit: 1000, hsmUsedLimit: 150 }
319
+ vi.mocked(client.get).mockResolvedValue(limit)
320
+ const result = await api.getHsmLimit('acc-1')
321
+ expect(client.get).toHaveBeenCalledWith('/whatsapp-business-templates/acc-1/hsmLimit')
322
+ expect(result).toEqual(limit)
323
+ })
324
+ })
325
+
326
+ // ── Error propagation ────────────────────────────────────────────
327
+
328
+ describe('error propagation', () => {
329
+ it('propagates errors from getMany', async () => {
330
+ vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
331
+ await expect(api.getMany()).rejects.toThrow('Network error')
332
+ })
333
+
334
+ it('propagates errors from sendById', async () => {
335
+ vi.mocked(client.post).mockRejectedValue(new Error('Conflict'))
336
+ await expect(api.sendById('cmp-1')).rejects.toThrow('Conflict')
337
+ })
338
+
339
+ it('propagates errors from getHsmLimit', async () => {
340
+ vi.mocked(client.get).mockRejectedValue(new Error('Forbidden'))
341
+ await expect(api.getHsmLimit('acc-1')).rejects.toThrow('Forbidden')
342
+ })
343
+ })
344
+ })
@@ -1,5 +1,5 @@
1
- import type { ApiClient } from '../../core/ApiClient'
2
- import { BaseCrudApi } from '../../core/BaseCrudApi'
1
+ import type { ApiClient } from '../../core'
2
+ import { BaseCrudApi } from '../../core'
3
3
  import type {
4
4
  AutoPauseModePayload,
5
5
  Campaign,
@@ -19,6 +19,18 @@ export class CampaignsApi extends BaseCrudApi<
19
19
  > {
20
20
  constructor(client: ApiClient) {
21
21
  super(client, '/campaigns')
22
+
23
+ this.sendById = this.sendById.bind(this)
24
+ this.pauseById = this.pauseById.bind(this)
25
+ this.resumeById = this.resumeById.bind(this)
26
+ this.duplicateById = this.duplicateById.bind(this)
27
+ this.getStatsById = this.getStatsById.bind(this)
28
+ this.setIntervalById = this.setIntervalById.bind(this)
29
+ this.getContactCount = this.getContactCount.bind(this)
30
+ this.setAutoPauseMode = this.setAutoPauseMode.bind(this)
31
+ this.exportCampaignResult = this.exportCampaignResult.bind(this)
32
+ this.exportTemplateCsv = this.exportTemplateCsv.bind(this)
33
+ this.getHsmLimit = this.getHsmLimit.bind(this)
22
34
  }
23
35
 
24
36
  /** Starts sending messages for the given campaign. */
@@ -1,2 +1,3 @@
1
1
  export { CampaignsApi } from './CampaignsApi'
2
+ export * as scopes from './scopes'
2
3
  export type * from './types'
@@ -0,0 +1,79 @@
1
+ import type { IncludeItem, ListQuery, WhereClause } from '../../../core'
2
+ import type { Campaign, CampaignStatus } from '../types'
3
+
4
+ export type GetManyPaginatedFilters = {
5
+ title?: string
6
+ status?: CampaignStatus
7
+ serviceId?: string
8
+ createdAtFrom?: string
9
+ createdAtTo?: string
10
+ startedAtFrom?: string
11
+ startedAtTo?: string
12
+ quality?: string
13
+ page?: number
14
+ perPage?: number
15
+ sort?: string
16
+ order?: 'ASC' | 'DESC'
17
+ paginate?: boolean
18
+ }
19
+
20
+ export function getManyViewScope(
21
+ filters: GetManyPaginatedFilters = {},
22
+ ): ListQuery<Campaign> & { paginate: true } {
23
+ const where: WhereClause<Campaign> = {}
24
+ const $and: WhereClause<Campaign>[] = []
25
+
26
+ if (filters.title) {
27
+ where.title = { $iLike: `%${filters.title}%` }
28
+ }
29
+ if (filters.status) {
30
+ where.status = filters.status
31
+ }
32
+ if (filters.serviceId) {
33
+ where.serviceId = filters.serviceId
34
+ }
35
+ if (filters.createdAtFrom) {
36
+ $and.push({ createdAt: { $gte: filters.createdAtFrom } })
37
+ }
38
+ if (filters.createdAtTo) {
39
+ $and.push({ createdAt: { $lte: filters.createdAtTo } })
40
+ }
41
+ if (filters.startedAtFrom) {
42
+ $and.push({ startedAt: { $gte: filters.startedAtFrom } })
43
+ }
44
+ if (filters.startedAtTo) {
45
+ $and.push({ startedAt: { $lte: filters.startedAtTo } })
46
+ }
47
+
48
+ if ($and.length > 0) {
49
+ where.$and = $and
50
+ }
51
+
52
+ const include: IncludeItem<Campaign>[] = [
53
+ 'service',
54
+ 'tags',
55
+ {
56
+ model: 'messages',
57
+ ...(filters.quality && { required: true }),
58
+ include: [
59
+ 'file',
60
+ {
61
+ model: 'hsm',
62
+ ...(filters.quality && { where: { quality: filters.quality }, required: true }),
63
+ },
64
+ ],
65
+ },
66
+ { model: 'createdBy', attributes: ['id', 'name'] },
67
+ { model: 'sentBy', attributes: ['id', 'name'] },
68
+ ]
69
+
70
+ return {
71
+ where,
72
+ order: [[(filters.sort ?? 'createdAt') as keyof Campaign & string, filters.order ?? 'DESC']],
73
+ page: filters.page ?? 1,
74
+ perPage: filters.perPage ?? 15,
75
+ paginate: true as const,
76
+ ...(filters.quality && { subQuery: false }),
77
+ include,
78
+ }
79
+ }
@@ -0,0 +1,21 @@
1
+ import type { GetByIdQuery } from '../../../core'
2
+ import type { Campaign } from '../types'
3
+
4
+ /** Query scope that includes all associations for a detailed campaign view. */
5
+ export function getOneViewScope(): GetByIdQuery<Campaign> {
6
+ return {
7
+ include: [
8
+ 'service',
9
+ 'tags',
10
+ {
11
+ model: 'messages',
12
+ include: ['file', 'hsm', 'hsmFile'],
13
+ },
14
+ 'importFile',
15
+ 'defaultDepartment',
16
+ 'defaultUser',
17
+ 'createdBy',
18
+ 'sentBy',
19
+ ],
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ export { getOneViewScope } from './getOneViewScope'
2
+ export { getManyViewScope, type GetManyPaginatedFilters } from './getManyViewScope'
@@ -1,7 +1,9 @@
1
- import type { Department } from '../departments/types'
2
- import type { Service } from '../services/types'
3
- import type { Tag } from '../tags/types'
4
- import type { User } from '../users/types'
1
+ import type { Department } from '../departments'
2
+ import type { File } from '../files'
3
+ import type { Message } from '../messages'
4
+ import type { Service } from '../services'
5
+ import type { Tag } from '../tags'
6
+ import type { User } from '../users'
5
7
 
6
8
  export type CampaignStatus =
7
9
  | 'ready'
@@ -47,6 +49,9 @@ export type Campaign = {
47
49
  totalContacts: number | null
48
50
  totalContactsImported: number | null
49
51
  totalValidContacts: number | null
52
+ sentContacts: number | null
53
+ viewedContacts: number | null
54
+ contactCount: number | null
50
55
  mustOpenTicket: boolean
51
56
  marketingIntegration: boolean
52
57
  accountId: string
@@ -65,6 +70,8 @@ export type Campaign = {
65
70
  createdBy?: User | null
66
71
  sentBy?: User | null
67
72
  tags?: Tag[]
73
+ messages?: Message[]
74
+ importFile?: File | null
68
75
  }
69
76
 
70
77
  export type CampaignRelationships =
@@ -74,28 +81,33 @@ export type CampaignRelationships =
74
81
  | 'createdBy'
75
82
  | 'sentBy'
76
83
  | 'tags'
84
+ | 'messages'
85
+ | 'importFile'
77
86
 
78
87
  export type CreateCampaignPayload = {
79
88
  title: string
80
89
  serviceId: string
81
- config: CampaignConfig
82
- defaultDepartmentId?: string
83
- defaultUserId?: string
84
- sendsAt?: string
90
+ config?: { minInterval: number | string; maxInterval: number | string }
91
+ tagIds?: string[]
92
+ messages?: { text?: string; fileId?: string }[]
85
93
  isScheduled?: boolean
94
+ sendsAt?: string | null
86
95
  mustOpenTicket?: boolean
87
- }
88
-
89
- export type UpdateCampaignPayload = {
90
- title?: string
91
- config?: CampaignConfig
92
96
  defaultDepartmentId?: string | null
93
97
  defaultUserId?: string | null
94
- sendsAt?: string | null
95
- isScheduled?: boolean
96
- mustOpenTicket?: boolean
98
+ hsm?: unknown
99
+ parameters?: unknown
100
+ fileTemplate?: unknown
101
+ contacts?: unknown[]
102
+ useTags?: boolean
103
+ marketingIntegration?: boolean
104
+ fileId?: string
105
+ nameField?: string
106
+ numberField?: string
97
107
  }
98
108
 
109
+ export type UpdateCampaignPayload = Partial<CreateCampaignPayload>
110
+
99
111
  export type CampaignStatsInfo = {
100
112
  totalValidContacts: number
101
113
  totalContacts: number
@@ -0,0 +1,76 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { ClientFeedbackApi } from './ClientFeedbackApi'
3
+ import type { ApiClient } from '../../core/ApiClient'
4
+ import type { ClientFeedback, CreateClientFeedbackPayload } 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
+ // ── Fixtures ─────────────────────────────────────────────────────────
21
+
22
+ const feedback: ClientFeedback = {
23
+ id: 'cf-1',
24
+ accountId: 'acc-1',
25
+ ticketId: 'ticket-1',
26
+ feedback: 'Great service!',
27
+ createdAt: '2024-01-01T00:00:00Z',
28
+ updatedAt: '2024-01-01T00:00:00Z',
29
+ }
30
+
31
+ // ── Tests ────────────────────────────────────────────────────────────
32
+
33
+ describe('ClientFeedbackApi', () => {
34
+ let client: ReturnType<typeof createMockClient>
35
+ let api: ClientFeedbackApi
36
+
37
+ beforeEach(() => {
38
+ client = createMockClient()
39
+ api = new ClientFeedbackApi(client)
40
+ })
41
+
42
+ // ── create ───────────────────────────────────────────────────────
43
+
44
+ describe('create', () => {
45
+ it('calls client.post with /client-feedback and body', async () => {
46
+ vi.mocked(client.post).mockResolvedValue(feedback)
47
+ const body: CreateClientFeedbackPayload = {
48
+ accountId: 'acc-1',
49
+ ticketId: 'ticket-1',
50
+ feedback: 'Great service!',
51
+ }
52
+ const result = await api.create(body)
53
+ expect(client.post).toHaveBeenCalledWith('/client-feedback', body, undefined)
54
+ expect(result).toEqual(feedback)
55
+ })
56
+
57
+ it('forwards custom headers', async () => {
58
+ vi.mocked(client.post).mockResolvedValue(feedback)
59
+ const headers = { 'X-Custom': 'h' }
60
+ const body: CreateClientFeedbackPayload = {
61
+ accountId: 'acc-1',
62
+ ticketId: 'ticket-1',
63
+ feedback: 'Good',
64
+ }
65
+ await api.create(body, headers)
66
+ expect(client.post).toHaveBeenCalledWith('/client-feedback', body, headers)
67
+ })
68
+
69
+ it('propagates errors from client.post', async () => {
70
+ vi.mocked(client.post).mockRejectedValue(new Error('Bad request'))
71
+ await expect(
72
+ api.create({ accountId: 'acc-1', ticketId: 't-1', feedback: 'x' }),
73
+ ).rejects.toThrow('Bad request')
74
+ })
75
+ })
76
+ })