@app-connect/core 1.7.8 → 1.7.11

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 (69) hide show
  1. package/connector/developerPortal.js +43 -0
  2. package/connector/proxy/index.js +10 -3
  3. package/connector/registry.js +8 -6
  4. package/handlers/admin.js +44 -21
  5. package/handlers/auth.js +97 -69
  6. package/handlers/calldown.js +10 -4
  7. package/handlers/contact.js +45 -112
  8. package/handlers/disposition.js +4 -142
  9. package/handlers/log.js +174 -259
  10. package/handlers/user.js +19 -6
  11. package/index.js +310 -122
  12. package/lib/analytics.js +3 -1
  13. package/lib/authSession.js +68 -0
  14. package/lib/callLogComposer.js +498 -420
  15. package/lib/errorHandler.js +206 -0
  16. package/lib/jwt.js +2 -0
  17. package/lib/logger.js +190 -0
  18. package/lib/oauth.js +21 -12
  19. package/lib/ringcentral.js +2 -10
  20. package/lib/sharedSMSComposer.js +471 -0
  21. package/mcp/SupportedPlatforms.md +12 -0
  22. package/mcp/lib/validator.js +91 -0
  23. package/mcp/mcpHandler.js +166 -0
  24. package/mcp/tools/checkAuthStatus.js +90 -0
  25. package/mcp/tools/collectAuthInfo.js +86 -0
  26. package/mcp/tools/createCallLog.js +299 -0
  27. package/mcp/tools/createMessageLog.js +283 -0
  28. package/mcp/tools/doAuth.js +185 -0
  29. package/mcp/tools/findContactByName.js +87 -0
  30. package/mcp/tools/findContactByPhone.js +96 -0
  31. package/mcp/tools/getCallLog.js +98 -0
  32. package/mcp/tools/getHelp.js +39 -0
  33. package/mcp/tools/getPublicConnectors.js +46 -0
  34. package/mcp/tools/index.js +58 -0
  35. package/mcp/tools/logout.js +63 -0
  36. package/mcp/tools/rcGetCallLogs.js +73 -0
  37. package/mcp/tools/setConnector.js +64 -0
  38. package/mcp/tools/updateCallLog.js +122 -0
  39. package/models/accountDataModel.js +34 -0
  40. package/models/cacheModel.js +3 -0
  41. package/package.json +6 -4
  42. package/releaseNotes.json +36 -0
  43. package/test/connector/registry.test.js +145 -0
  44. package/test/handlers/admin.test.js +583 -0
  45. package/test/handlers/auth.test.js +355 -0
  46. package/test/handlers/contact.test.js +852 -0
  47. package/test/handlers/log.test.js +872 -0
  48. package/test/lib/callLogComposer.test.js +1231 -0
  49. package/test/lib/debugTracer.test.js +328 -0
  50. package/test/lib/logger.test.js +206 -0
  51. package/test/lib/oauth.test.js +359 -0
  52. package/test/lib/ringcentral.test.js +473 -0
  53. package/test/lib/sharedSMSComposer.test.js +1084 -0
  54. package/test/lib/util.test.js +282 -0
  55. package/test/mcp/tools/collectAuthInfo.test.js +192 -0
  56. package/test/mcp/tools/createCallLog.test.js +412 -0
  57. package/test/mcp/tools/createMessageLog.test.js +580 -0
  58. package/test/mcp/tools/doAuth.test.js +363 -0
  59. package/test/mcp/tools/findContactByName.test.js +263 -0
  60. package/test/mcp/tools/findContactByPhone.test.js +284 -0
  61. package/test/mcp/tools/getCallLog.test.js +286 -0
  62. package/test/mcp/tools/getPublicConnectors.test.js +128 -0
  63. package/test/mcp/tools/logout.test.js +169 -0
  64. package/test/mcp/tools/setConnector.test.js +177 -0
  65. package/test/mcp/tools/updateCallLog.test.js +346 -0
  66. package/test/models/accountDataModel.test.js +98 -0
  67. package/test/models/dynamo/connectorSchema.test.js +189 -0
  68. package/test/models/models.test.js +539 -0
  69. package/test/setup.js +176 -176
@@ -0,0 +1,872 @@
1
+ // Use in-memory SQLite for isolated model tests
2
+ jest.mock('../../models/sequelize', () => {
3
+ const { Sequelize } = require('sequelize');
4
+ return {
5
+ sequelize: new Sequelize({
6
+ dialect: 'sqlite',
7
+ storage: ':memory:',
8
+ logging: false,
9
+ }),
10
+ };
11
+ });
12
+
13
+ jest.mock('../../connector/registry');
14
+ jest.mock('../../lib/oauth');
15
+ jest.mock('../../lib/callLogComposer');
16
+ jest.mock('../../models/dynamo/noteCacheSchema', () => ({
17
+ NoteCache: {
18
+ get: jest.fn(),
19
+ create: jest.fn()
20
+ }
21
+ }));
22
+ jest.mock('../../models/dynamo/connectorSchema', () => ({
23
+ Connector: {
24
+ getProxyConfig: jest.fn()
25
+ }
26
+ }));
27
+
28
+ const logHandler = require('../../handlers/log');
29
+ const { CallLogModel } = require('../../models/callLogModel');
30
+ const { MessageLogModel } = require('../../models/messageLogModel');
31
+ const { UserModel } = require('../../models/userModel');
32
+ const connectorRegistry = require('../../connector/registry');
33
+ const oauth = require('../../lib/oauth');
34
+ const { composeCallLog } = require('../../lib/callLogComposer');
35
+ const { NoteCache } = require('../../models/dynamo/noteCacheSchema');
36
+ const { sequelize } = require('../../models/sequelize');
37
+
38
+ describe('Log Handler', () => {
39
+ beforeAll(async () => {
40
+ await CallLogModel.sync({ force: true });
41
+ await MessageLogModel.sync({ force: true });
42
+ await UserModel.sync({ force: true });
43
+ });
44
+
45
+ afterEach(async () => {
46
+ await CallLogModel.destroy({ where: {} });
47
+ await MessageLogModel.destroy({ where: {} });
48
+ await UserModel.destroy({ where: {} });
49
+ jest.clearAllMocks();
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await sequelize.close();
54
+ });
55
+
56
+ describe('createCallLog', () => {
57
+ const mockUser = {
58
+ id: 'test-user-id',
59
+ platform: 'testCRM',
60
+ accessToken: 'test-access-token',
61
+ platformAdditionalInfo: {}
62
+ };
63
+
64
+ const mockIncomingData = {
65
+ logInfo: {
66
+ sessionId: 'session-123',
67
+ telephonySessionId: 'tel-session-123',
68
+ id: 'call-id-123',
69
+ direction: 'Outbound',
70
+ startTime: new Date().toISOString(),
71
+ duration: 120,
72
+ result: 'Completed',
73
+ from: { phoneNumber: '+1234567890' },
74
+ to: { phoneNumber: '+0987654321' },
75
+ recording: { link: 'https://recording.link' }
76
+ },
77
+ contactId: 'contact-123',
78
+ contactType: 'Contact',
79
+ contactName: 'Test Contact',
80
+ note: 'Test note',
81
+ aiNote: '',
82
+ transcript: '',
83
+ additionalSubmission: {}
84
+ };
85
+
86
+ test('should return warning when call log already exists for session', async () => {
87
+ // Arrange
88
+ await CallLogModel.create({
89
+ id: 'existing-log',
90
+ sessionId: 'session-123',
91
+ platform: 'testCRM',
92
+ thirdPartyLogId: 'third-party-123',
93
+ userId: 'test-user-id'
94
+ });
95
+
96
+ // Act
97
+ const result = await logHandler.createCallLog({
98
+ platform: 'testCRM',
99
+ userId: 'test-user-id',
100
+ incomingData: mockIncomingData,
101
+ hashedAccountId: 'hashed-123',
102
+ isFromSSCL: false
103
+ });
104
+
105
+ // Assert
106
+ expect(result.successful).toBe(false);
107
+ expect(result.returnMessage.messageType).toBe('warning');
108
+ expect(result.returnMessage.message).toContain('Existing log for session');
109
+ });
110
+
111
+ test('should return warning when user not found', async () => {
112
+ // Act
113
+ const result = await logHandler.createCallLog({
114
+ platform: 'testCRM',
115
+ userId: 'non-existent-user',
116
+ incomingData: mockIncomingData,
117
+ hashedAccountId: 'hashed-123',
118
+ isFromSSCL: false
119
+ });
120
+
121
+ // Assert
122
+ expect(result.successful).toBe(false);
123
+ expect(result.returnMessage.message).toBe('User not found');
124
+ });
125
+
126
+ test('should return warning when user has no access token', async () => {
127
+ // Arrange
128
+ await UserModel.create({
129
+ id: 'test-user-id',
130
+ platform: 'testCRM',
131
+ accessToken: null
132
+ });
133
+
134
+ // Act
135
+ const result = await logHandler.createCallLog({
136
+ platform: 'testCRM',
137
+ userId: 'test-user-id',
138
+ incomingData: mockIncomingData,
139
+ hashedAccountId: 'hashed-123',
140
+ isFromSSCL: false
141
+ });
142
+
143
+ // Assert
144
+ expect(result.successful).toBe(false);
145
+ expect(result.returnMessage.message).toBe('User not found');
146
+ });
147
+
148
+ test('should return warning when contact not found', async () => {
149
+ // Arrange
150
+ await UserModel.create(mockUser);
151
+
152
+ const mockConnector = {
153
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
154
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
155
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
156
+ createCallLog: jest.fn()
157
+ };
158
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
159
+ composeCallLog.mockReturnValue('Composed log details');
160
+
161
+ const incomingDataNoContact = {
162
+ ...mockIncomingData,
163
+ contactId: null
164
+ };
165
+
166
+ // Act
167
+ const result = await logHandler.createCallLog({
168
+ platform: 'testCRM',
169
+ userId: 'test-user-id',
170
+ incomingData: incomingDataNoContact,
171
+ hashedAccountId: 'hashed-123',
172
+ isFromSSCL: false
173
+ });
174
+
175
+ // Assert
176
+ expect(result.successful).toBe(false);
177
+ expect(result.returnMessage.message).toContain('Contact not found for number');
178
+ });
179
+
180
+ test('should successfully create call log with apiKey auth', async () => {
181
+ // Arrange
182
+ await UserModel.create(mockUser);
183
+
184
+ const mockConnector = {
185
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
186
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
187
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
188
+ createCallLog: jest.fn().mockResolvedValue({
189
+ logId: 'new-log-123',
190
+ returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
191
+ })
192
+ };
193
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
194
+ composeCallLog.mockReturnValue('Composed log details');
195
+
196
+ // Act
197
+ const result = await logHandler.createCallLog({
198
+ platform: 'testCRM',
199
+ userId: 'test-user-id',
200
+ incomingData: mockIncomingData,
201
+ hashedAccountId: 'hashed-123',
202
+ isFromSSCL: false
203
+ });
204
+
205
+ // Assert
206
+ expect(result.successful).toBe(true);
207
+ expect(result.logId).toBe('new-log-123');
208
+ expect(mockConnector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'test-access-token' });
209
+ expect(mockConnector.createCallLog).toHaveBeenCalled();
210
+
211
+ // Verify call log was saved to database
212
+ const savedLog = await CallLogModel.findOne({ where: { sessionId: 'session-123' } });
213
+ expect(savedLog).not.toBeNull();
214
+ expect(savedLog.thirdPartyLogId).toBe('new-log-123');
215
+ });
216
+
217
+ test('should successfully create call log with oauth auth', async () => {
218
+ // Arrange
219
+ const oauthUser = { ...mockUser };
220
+ await UserModel.create(oauthUser);
221
+
222
+ const mockConnector = {
223
+ getAuthType: jest.fn().mockResolvedValue('oauth'),
224
+ getOauthInfo: jest.fn().mockResolvedValue({
225
+ clientId: 'client-id',
226
+ clientSecret: 'client-secret',
227
+ accessTokenUri: 'https://token.url',
228
+ authorizationUri: 'https://auth.url'
229
+ }),
230
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
231
+ createCallLog: jest.fn().mockResolvedValue({
232
+ logId: 'oauth-log-123',
233
+ returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
234
+ })
235
+ };
236
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
237
+
238
+ const mockOAuthApp = {};
239
+ oauth.getOAuthApp.mockReturnValue(mockOAuthApp);
240
+ oauth.checkAndRefreshAccessToken.mockResolvedValue({
241
+ ...oauthUser,
242
+ accessToken: 'refreshed-token'
243
+ });
244
+ composeCallLog.mockReturnValue('Composed log details');
245
+
246
+ // Act
247
+ const result = await logHandler.createCallLog({
248
+ platform: 'testCRM',
249
+ userId: 'test-user-id',
250
+ incomingData: mockIncomingData,
251
+ hashedAccountId: 'hashed-123',
252
+ isFromSSCL: false
253
+ });
254
+
255
+ // Assert
256
+ expect(result.successful).toBe(true);
257
+ expect(result.logId).toBe('oauth-log-123');
258
+ expect(oauth.checkAndRefreshAccessToken).toHaveBeenCalled();
259
+ });
260
+
261
+ test('should use cached note when USE_CACHE is enabled and isFromSSCL', async () => {
262
+ // Arrange
263
+ process.env.USE_CACHE = 'true';
264
+ await UserModel.create(mockUser);
265
+
266
+ NoteCache.get.mockResolvedValue({ note: 'Cached note' });
267
+
268
+ const mockConnector = {
269
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
270
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
271
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
272
+ createCallLog: jest.fn().mockResolvedValue({
273
+ logId: 'cached-log-123',
274
+ returnMessage: { message: 'Call logged', messageType: 'success', ttl: 2000 }
275
+ })
276
+ };
277
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
278
+ composeCallLog.mockReturnValue('Composed log details');
279
+
280
+ // Act
281
+ const result = await logHandler.createCallLog({
282
+ platform: 'testCRM',
283
+ userId: 'test-user-id',
284
+ incomingData: mockIncomingData,
285
+ hashedAccountId: 'hashed-123',
286
+ isFromSSCL: true
287
+ });
288
+
289
+ // Assert
290
+ expect(result.successful).toBe(true);
291
+ expect(NoteCache.get).toHaveBeenCalledWith({ sessionId: 'session-123' });
292
+
293
+ // Clean up
294
+ delete process.env.USE_CACHE;
295
+ });
296
+ });
297
+
298
+ describe('getCallLog', () => {
299
+ test('should return error when user not found', async () => {
300
+ // Act
301
+ const result = await logHandler.getCallLog({
302
+ userId: 'non-existent-user',
303
+ sessionIds: 'session-1,session-2',
304
+ platform: 'testCRM',
305
+ requireDetails: false
306
+ });
307
+
308
+ // Assert
309
+ expect(result.successful).toBe(false);
310
+ expect(result.message).toBe('Contact not found');
311
+ });
312
+
313
+ test('should return error when no session IDs provided', async () => {
314
+ // Arrange
315
+ await UserModel.create({
316
+ id: 'test-user-id',
317
+ platform: 'testCRM',
318
+ accessToken: 'test-token'
319
+ });
320
+
321
+ // Act
322
+ const result = await logHandler.getCallLog({
323
+ userId: 'test-user-id',
324
+ sessionIds: null,
325
+ platform: 'testCRM',
326
+ requireDetails: false
327
+ });
328
+
329
+ // Assert
330
+ expect(result.successful).toBe(false);
331
+ expect(result.message).toBe('No session IDs provided');
332
+ });
333
+
334
+ test('should return matched logs without details', async () => {
335
+ // Arrange
336
+ await UserModel.create({
337
+ id: 'test-user-id',
338
+ platform: 'testCRM',
339
+ accessToken: 'test-token'
340
+ });
341
+
342
+ await CallLogModel.create({
343
+ id: 'call-1',
344
+ sessionId: 'session-1',
345
+ platform: 'testCRM',
346
+ thirdPartyLogId: 'log-1',
347
+ userId: 'test-user-id'
348
+ });
349
+
350
+ // Act
351
+ const result = await logHandler.getCallLog({
352
+ userId: 'test-user-id',
353
+ sessionIds: 'session-1,session-2',
354
+ platform: 'testCRM',
355
+ requireDetails: false
356
+ });
357
+
358
+ // Assert
359
+ expect(result.successful).toBe(true);
360
+ expect(result.logs).toHaveLength(2);
361
+ expect(result.logs[0]).toEqual({
362
+ sessionId: 'session-1',
363
+ matched: true,
364
+ logId: 'log-1'
365
+ });
366
+ expect(result.logs[1]).toEqual({
367
+ sessionId: 'session-2',
368
+ matched: false
369
+ });
370
+ });
371
+
372
+ test('should return matched logs with details when requireDetails is true', async () => {
373
+ // Arrange
374
+ await UserModel.create({
375
+ id: 'test-user-id',
376
+ platform: 'testCRM',
377
+ accessToken: 'test-token',
378
+ platformAdditionalInfo: {}
379
+ });
380
+
381
+ await CallLogModel.create({
382
+ id: 'call-1',
383
+ sessionId: 'session-1',
384
+ platform: 'testCRM',
385
+ thirdPartyLogId: 'log-1',
386
+ userId: 'test-user-id',
387
+ contactId: 'contact-1'
388
+ });
389
+
390
+ const mockConnector = {
391
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
392
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
393
+ getCallLog: jest.fn().mockResolvedValue({
394
+ callLogInfo: { subject: 'Test Call', note: 'Test note' },
395
+ returnMessage: { message: 'Success', messageType: 'success' }
396
+ })
397
+ };
398
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
399
+
400
+ // Act
401
+ const result = await logHandler.getCallLog({
402
+ userId: 'test-user-id',
403
+ sessionIds: 'session-1',
404
+ platform: 'testCRM',
405
+ requireDetails: true
406
+ });
407
+
408
+ // Assert
409
+ expect(result.successful).toBe(true);
410
+ expect(result.logs).toHaveLength(1);
411
+ expect(result.logs[0].matched).toBe(true);
412
+ expect(result.logs[0].logData).toEqual({ subject: 'Test Call', note: 'Test note' });
413
+ expect(mockConnector.getCallLog).toHaveBeenCalled();
414
+ });
415
+
416
+ test('should limit session IDs to 5', async () => {
417
+ // Arrange
418
+ await UserModel.create({
419
+ id: 'test-user-id',
420
+ platform: 'testCRM',
421
+ accessToken: 'test-token'
422
+ });
423
+
424
+ const sessionIds = 'session-1,session-2,session-3,session-4,session-5,session-6,session-7';
425
+
426
+ // Act
427
+ const result = await logHandler.getCallLog({
428
+ userId: 'test-user-id',
429
+ sessionIds,
430
+ platform: 'testCRM',
431
+ requireDetails: false
432
+ });
433
+
434
+ // Assert
435
+ expect(result.successful).toBe(true);
436
+ expect(result.logs).toHaveLength(5);
437
+ });
438
+
439
+ test('should skip session ID 0', async () => {
440
+ // Arrange
441
+ await UserModel.create({
442
+ id: 'test-user-id',
443
+ platform: 'testCRM',
444
+ accessToken: 'test-token',
445
+ platformAdditionalInfo: {}
446
+ });
447
+
448
+ const mockConnector = {
449
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
450
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
451
+ getCallLog: jest.fn()
452
+ };
453
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
454
+
455
+ // Act
456
+ const result = await logHandler.getCallLog({
457
+ userId: 'test-user-id',
458
+ sessionIds: '0,session-1',
459
+ platform: 'testCRM',
460
+ requireDetails: true
461
+ });
462
+
463
+ // Assert
464
+ expect(result.successful).toBe(true);
465
+ expect(result.logs[0]).toEqual({ sessionId: '0', matched: false });
466
+ });
467
+ });
468
+
469
+ describe('updateCallLog', () => {
470
+ test('should return unsuccessful when no existing call log found', async () => {
471
+ // Act
472
+ const result = await logHandler.updateCallLog({
473
+ platform: 'testCRM',
474
+ userId: 'test-user-id',
475
+ incomingData: { sessionId: 'non-existent-session' },
476
+ hashedAccountId: 'hashed-123',
477
+ isFromSSCL: false
478
+ });
479
+
480
+ // Assert
481
+ expect(result.successful).toBe(false);
482
+ });
483
+
484
+ test('should return error when user not found for update', async () => {
485
+ // Arrange
486
+ await CallLogModel.create({
487
+ id: 'call-1',
488
+ sessionId: 'session-1',
489
+ platform: 'testCRM',
490
+ thirdPartyLogId: 'log-1',
491
+ userId: 'test-user-id'
492
+ });
493
+
494
+ // Act
495
+ const result = await logHandler.updateCallLog({
496
+ platform: 'testCRM',
497
+ userId: 'non-existent-user',
498
+ incomingData: { sessionId: 'session-1' },
499
+ hashedAccountId: 'hashed-123',
500
+ isFromSSCL: false
501
+ });
502
+
503
+ // Assert
504
+ expect(result.successful).toBe(false);
505
+ expect(result.message).toBe('Contact not found');
506
+ });
507
+
508
+ test('should successfully update call log', async () => {
509
+ // Arrange
510
+ await UserModel.create({
511
+ id: 'test-user-id',
512
+ platform: 'testCRM',
513
+ accessToken: 'test-token',
514
+ platformAdditionalInfo: {}
515
+ });
516
+
517
+ await CallLogModel.create({
518
+ id: 'call-1',
519
+ sessionId: 'session-1',
520
+ platform: 'testCRM',
521
+ thirdPartyLogId: 'log-1',
522
+ userId: 'test-user-id',
523
+ contactId: 'contact-1'
524
+ });
525
+
526
+ const mockConnector = {
527
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
528
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
529
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
530
+ getCallLog: jest.fn().mockResolvedValue({
531
+ callLogInfo: { fullBody: 'Existing body', note: 'Existing note' }
532
+ }),
533
+ updateCallLog: jest.fn().mockResolvedValue({
534
+ updatedNote: 'Updated note',
535
+ returnMessage: { message: 'Updated', messageType: 'success', ttl: 2000 }
536
+ })
537
+ };
538
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
539
+ composeCallLog.mockReturnValue('Updated composed log');
540
+
541
+ const incomingData = {
542
+ sessionId: 'session-1',
543
+ note: 'Updated note',
544
+ subject: 'Updated subject',
545
+ startTime: new Date().toISOString(),
546
+ duration: 180,
547
+ result: 'Completed'
548
+ };
549
+
550
+ // Act
551
+ const result = await logHandler.updateCallLog({
552
+ platform: 'testCRM',
553
+ userId: 'test-user-id',
554
+ incomingData,
555
+ hashedAccountId: 'hashed-123',
556
+ isFromSSCL: false
557
+ });
558
+
559
+ // Assert
560
+ expect(result.successful).toBe(true);
561
+ expect(result.logId).toBe('log-1');
562
+ expect(result.updatedNote).toBe('Updated note');
563
+ expect(mockConnector.updateCallLog).toHaveBeenCalled();
564
+ });
565
+ });
566
+
567
+ describe('createMessageLog', () => {
568
+ test('should return warning when no messages to log', async () => {
569
+ // Act
570
+ const result = await logHandler.createMessageLog({
571
+ platform: 'testCRM',
572
+ userId: 'test-user-id',
573
+ incomingData: {
574
+ logInfo: { messages: [] }
575
+ }
576
+ });
577
+
578
+ // Assert
579
+ expect(result.successful).toBe(false);
580
+ expect(result.returnMessage.message).toBe('No message to log.');
581
+ });
582
+
583
+ test('should return warning when user not found', async () => {
584
+ // Act
585
+ const result = await logHandler.createMessageLog({
586
+ platform: 'testCRM',
587
+ userId: 'non-existent-user',
588
+ incomingData: {
589
+ logInfo: {
590
+ messages: [{ id: 'msg-1', subject: 'Test', creationTime: new Date() }],
591
+ correspondents: [{ phoneNumber: '+1234567890' }]
592
+ }
593
+ }
594
+ });
595
+
596
+ // Assert
597
+ expect(result.successful).toBe(false);
598
+ expect(result.returnMessage.message).toBe('Contact not found');
599
+ });
600
+
601
+ test('should return warning when contact not found', async () => {
602
+ // Arrange
603
+ await UserModel.create({
604
+ id: 'test-user-id',
605
+ platform: 'testCRM',
606
+ accessToken: 'test-token',
607
+ platformAdditionalInfo: {}
608
+ });
609
+
610
+ const mockConnector = {
611
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
612
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded')
613
+ };
614
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
615
+
616
+ // Act
617
+ const result = await logHandler.createMessageLog({
618
+ platform: 'testCRM',
619
+ userId: 'test-user-id',
620
+ incomingData: {
621
+ logInfo: {
622
+ messages: [{ id: 'msg-1', subject: 'Test', creationTime: new Date() }],
623
+ correspondents: [{ phoneNumber: '+1234567890' }]
624
+ },
625
+ contactId: null
626
+ }
627
+ });
628
+
629
+ // Assert
630
+ expect(result.successful).toBe(false);
631
+ expect(result.returnMessage.message).toContain('Contact not found for number');
632
+ });
633
+
634
+ test('should successfully create message log', async () => {
635
+ // Arrange
636
+ await UserModel.create({
637
+ id: 'test-user-id',
638
+ platform: 'testCRM',
639
+ accessToken: 'test-token',
640
+ platformAdditionalInfo: {}
641
+ });
642
+
643
+ const mockConnector = {
644
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
645
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
646
+ createMessageLog: jest.fn().mockResolvedValue({
647
+ logId: 'msg-log-123',
648
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
649
+ }),
650
+ updateMessageLog: jest.fn()
651
+ };
652
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
653
+
654
+ const incomingData = {
655
+ logInfo: {
656
+ messages: [{ id: 'msg-1', subject: 'Test SMS', direction: 'Outbound', creationTime: new Date() }],
657
+ correspondents: [{ phoneNumber: '+1234567890' }],
658
+ conversationId: 'conv-123',
659
+ conversationLogId: 'conv-log-123'
660
+ },
661
+ contactId: 'contact-123',
662
+ contactType: 'Contact',
663
+ contactName: 'Test Contact',
664
+ additionalSubmission: {}
665
+ };
666
+
667
+ // Act
668
+ const result = await logHandler.createMessageLog({
669
+ platform: 'testCRM',
670
+ userId: 'test-user-id',
671
+ incomingData
672
+ });
673
+
674
+ // Assert
675
+ expect(result.successful).toBe(true);
676
+ expect(result.logIds).toContain('msg-1');
677
+ expect(mockConnector.createMessageLog).toHaveBeenCalled();
678
+
679
+ // Verify message log was saved
680
+ const savedLog = await MessageLogModel.findOne({ where: { id: 'msg-1' } });
681
+ expect(savedLog).not.toBeNull();
682
+ expect(savedLog.thirdPartyLogId).toBe('msg-log-123');
683
+ });
684
+
685
+ test('should skip already logged messages', async () => {
686
+ // Arrange
687
+ await UserModel.create({
688
+ id: 'test-user-id',
689
+ platform: 'testCRM',
690
+ accessToken: 'test-token',
691
+ platformAdditionalInfo: {}
692
+ });
693
+
694
+ await MessageLogModel.create({
695
+ id: 'msg-1',
696
+ platform: 'testCRM',
697
+ conversationId: 'conv-123',
698
+ thirdPartyLogId: 'existing-log',
699
+ userId: 'test-user-id'
700
+ });
701
+
702
+ const mockConnector = {
703
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
704
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
705
+ createMessageLog: jest.fn().mockResolvedValue({
706
+ logId: 'msg-log-new',
707
+ returnMessage: { message: 'Message logged', messageType: 'success', ttl: 2000 }
708
+ }),
709
+ updateMessageLog: jest.fn().mockResolvedValue({
710
+ returnMessage: { message: 'Message updated', messageType: 'success', ttl: 2000 }
711
+ })
712
+ };
713
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
714
+
715
+ const incomingData = {
716
+ logInfo: {
717
+ messages: [
718
+ { id: 'msg-1', subject: 'Already logged', direction: 'Outbound', creationTime: new Date() },
719
+ { id: 'msg-2', subject: 'New message', direction: 'Outbound', creationTime: new Date() }
720
+ ],
721
+ correspondents: [{ phoneNumber: '+1234567890' }],
722
+ conversationId: 'conv-123',
723
+ conversationLogId: 'new-conv-log-123'
724
+ },
725
+ contactId: 'contact-123',
726
+ contactType: 'Contact',
727
+ additionalSubmission: {}
728
+ };
729
+
730
+ // Act
731
+ const result = await logHandler.createMessageLog({
732
+ platform: 'testCRM',
733
+ userId: 'test-user-id',
734
+ incomingData
735
+ });
736
+
737
+ // Assert
738
+ expect(result.successful).toBe(true);
739
+ // msg-1 is skipped (already logged), msg-2 uses updateMessageLog because same conversationId exists
740
+ expect(mockConnector.createMessageLog).toHaveBeenCalledTimes(0);
741
+ expect(mockConnector.updateMessageLog).toHaveBeenCalledTimes(1);
742
+ });
743
+ });
744
+
745
+ describe('saveNoteCache', () => {
746
+ test('should successfully save note cache', async () => {
747
+ // Arrange
748
+ NoteCache.create.mockResolvedValue({ sessionId: 'session-123', note: 'Test note' });
749
+
750
+ // Act
751
+ const result = await logHandler.saveNoteCache({
752
+ sessionId: 'session-123',
753
+ note: 'Test note'
754
+ });
755
+
756
+ // Assert
757
+ expect(result.successful).toBe(true);
758
+ expect(result.returnMessage).toBe('Note cache saved');
759
+ expect(NoteCache.create).toHaveBeenCalled();
760
+ });
761
+
762
+ test('should handle errors when saving note cache', async () => {
763
+ // Arrange
764
+ NoteCache.create.mockRejectedValue(new Error('DynamoDB error'));
765
+
766
+ // Act
767
+ const result = await logHandler.saveNoteCache({
768
+ sessionId: 'session-123',
769
+ note: 'Test note'
770
+ });
771
+
772
+ // Assert
773
+ expect(result.successful).toBe(false);
774
+ expect(result.returnMessage).toBe('Error saving note cache');
775
+ });
776
+ });
777
+
778
+ describe('Error Handling', () => {
779
+ test('should handle 429 rate limit error in createCallLog', async () => {
780
+ // Arrange
781
+ await UserModel.create({
782
+ id: 'test-user-id',
783
+ platform: 'testCRM',
784
+ accessToken: 'test-token',
785
+ platformAdditionalInfo: {}
786
+ });
787
+
788
+ const mockConnector = {
789
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
790
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
791
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
792
+ createCallLog: jest.fn().mockRejectedValue({
793
+ response: { status: 429 }
794
+ })
795
+ };
796
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
797
+ composeCallLog.mockReturnValue('Composed log');
798
+
799
+ const incomingData = {
800
+ logInfo: {
801
+ sessionId: 'session-rate-limit',
802
+ telephonySessionId: 'tel-session',
803
+ direction: 'Outbound',
804
+ from: { phoneNumber: '+1234567890' },
805
+ to: { phoneNumber: '+0987654321' }
806
+ },
807
+ contactId: 'contact-123',
808
+ note: 'Test'
809
+ };
810
+
811
+ // Act
812
+ const result = await logHandler.createCallLog({
813
+ platform: 'testCRM',
814
+ userId: 'test-user-id',
815
+ incomingData,
816
+ hashedAccountId: 'hashed-123',
817
+ isFromSSCL: false
818
+ });
819
+
820
+ // Assert
821
+ expect(result.successful).toBe(false);
822
+ expect(result.returnMessage.messageType).toBe('warning');
823
+ });
824
+
825
+ test('should handle 401 authorization error in createCallLog', async () => {
826
+ // Arrange
827
+ await UserModel.create({
828
+ id: 'test-user-id',
829
+ platform: 'testCRM',
830
+ accessToken: 'test-token',
831
+ platformAdditionalInfo: {}
832
+ });
833
+
834
+ const mockConnector = {
835
+ getAuthType: jest.fn().mockResolvedValue('apiKey'),
836
+ getBasicAuth: jest.fn().mockReturnValue('base64-encoded'),
837
+ getLogFormatType: jest.fn().mockReturnValue('text/plain'),
838
+ createCallLog: jest.fn().mockRejectedValue({
839
+ response: { status: 401 }
840
+ })
841
+ };
842
+ connectorRegistry.getConnector.mockReturnValue(mockConnector);
843
+ composeCallLog.mockReturnValue('Composed log');
844
+
845
+ const incomingData = {
846
+ logInfo: {
847
+ sessionId: 'session-auth-error',
848
+ telephonySessionId: 'tel-session',
849
+ direction: 'Outbound',
850
+ from: { phoneNumber: '+1234567890' },
851
+ to: { phoneNumber: '+0987654321' }
852
+ },
853
+ contactId: 'contact-123',
854
+ note: 'Test'
855
+ };
856
+
857
+ // Act
858
+ const result = await logHandler.createCallLog({
859
+ platform: 'testCRM',
860
+ userId: 'test-user-id',
861
+ incomingData,
862
+ hashedAccountId: 'hashed-123',
863
+ isFromSSCL: false
864
+ });
865
+
866
+ // Assert
867
+ expect(result.successful).toBe(false);
868
+ expect(result.extraDataTracking.statusCode).toBe(401);
869
+ });
870
+ });
871
+ });
872
+