@webex/contact-center 3.9.0-next.2 → 3.9.0-next.4

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.
@@ -1,7 +1,11 @@
1
1
  import EventEmitter from 'events';
2
2
  import {CALL_EVENT_KEYS, LocalMicrophoneStream} from '@webex/calling';
3
3
  import {CallId} from '@webex/calling/dist/types/common/types';
4
- import {getErrorDetails} from '../core/Utils';
4
+ import {
5
+ getErrorDetails,
6
+ deriveConsultTransferDestinationType,
7
+ getDestinationAgentId,
8
+ } from '../core/Utils';
5
9
  import {LoginOption} from '../../types';
6
10
  import {TASK_FILE} from '../../constants';
7
11
  import {METHODS} from './constants';
@@ -19,7 +23,6 @@ import {
19
23
  ConsultEndPayload,
20
24
  TransferPayLoad,
21
25
  DESTINATION_TYPE,
22
- CONSULT_TRANSFER_DESTINATION_TYPE,
23
26
  ConsultTransferPayLoad,
24
27
  MEDIA_CHANNEL,
25
28
  } from './types';
@@ -1308,61 +1311,78 @@ export default class Task extends EventEmitter implements ITask {
1308
1311
  * ```
1309
1312
  */
1310
1313
  public async consultTransfer(
1311
- consultTransferPayload: ConsultTransferPayLoad
1314
+ consultTransferPayload?: ConsultTransferPayLoad
1312
1315
  ): Promise<TaskResponse> {
1313
1316
  try {
1314
- LoggerProxy.info(`Initiating consult transfer to ${consultTransferPayload.to}`, {
1315
- module: TASK_FILE,
1316
- method: METHODS.CONSULT_TRANSFER,
1317
- interactionId: this.data.interactionId,
1318
- });
1317
+ // Get the destination agent ID using custom logic from participants data
1318
+ const destAgentId = getDestinationAgentId(
1319
+ this.data.interaction?.participants,
1320
+ this.data.agentId
1321
+ );
1322
+
1323
+ // Resolve the target id (queue consult transfers go to the accepted agent)
1324
+ if (!destAgentId) {
1325
+ throw new Error('No agent has accepted this queue consult yet');
1326
+ }
1319
1327
 
1320
- // For queue destinations, use the destAgentId from task data
1321
- if (consultTransferPayload.destinationType === CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE) {
1322
- if (!this.data.destAgentId) {
1323
- throw new Error('No agent has accepted this queue consult yet');
1328
+ LoggerProxy.info(
1329
+ `Initiating consult transfer to ${consultTransferPayload?.to || destAgentId}`,
1330
+ {
1331
+ module: TASK_FILE,
1332
+ method: METHODS.CONSULT_TRANSFER,
1333
+ interactionId: this.data.interactionId,
1324
1334
  }
1335
+ );
1336
+ // Obtain payload based on desktop logic using TaskData
1337
+ const finalDestinationType = deriveConsultTransferDestinationType(this.data);
1325
1338
 
1326
- // Override the destination with the agent who accepted the queue consult
1327
- consultTransferPayload = {
1328
- to: this.data.destAgentId,
1329
- destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
1330
- };
1331
- }
1339
+ // By default we always use the computed destAgentId as the target id
1340
+ const consultTransferRequest: ConsultTransferPayLoad = {
1341
+ to: destAgentId,
1342
+ destinationType: finalDestinationType,
1343
+ };
1332
1344
 
1333
1345
  const result = await this.contact.consultTransfer({
1334
1346
  interactionId: this.data.interactionId,
1335
- data: consultTransferPayload,
1347
+ data: consultTransferRequest,
1336
1348
  });
1337
1349
 
1338
1350
  this.metricsManager.trackEvent(
1339
1351
  METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
1340
1352
  {
1341
1353
  taskId: this.data.interactionId,
1342
- destination: consultTransferPayload.to,
1343
- destinationType: consultTransferPayload.destinationType,
1354
+ destination: consultTransferRequest.to,
1355
+ destinationType: consultTransferRequest.destinationType,
1344
1356
  isConsultTransfer: true,
1345
1357
  ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
1346
1358
  },
1347
1359
  ['operational', 'behavioral', 'business']
1348
1360
  );
1349
1361
 
1350
- LoggerProxy.log(`Consult transfer completed successfully to ${consultTransferPayload.to}`, {
1351
- module: TASK_FILE,
1352
- method: METHODS.CONSULT_TRANSFER,
1353
- trackingId: result.trackingId,
1354
- interactionId: this.data.interactionId,
1355
- });
1362
+ LoggerProxy.log(
1363
+ `Consult transfer completed successfully to ${consultTransferPayload?.to || destAgentId}`,
1364
+ {
1365
+ module: TASK_FILE,
1366
+ method: METHODS.CONSULT_TRANSFER,
1367
+ trackingId: result.trackingId,
1368
+ interactionId: this.data.interactionId,
1369
+ }
1370
+ );
1356
1371
 
1357
1372
  return result;
1358
1373
  } catch (error) {
1359
1374
  const {error: detailedError} = getErrorDetails(error, METHODS.CONSULT_TRANSFER, TASK_FILE);
1375
+ const failedDestinationType = deriveConsultTransferDestinationType(this.data);
1376
+ const failedDestAgentId = getDestinationAgentId(
1377
+ this.data.interaction?.participants,
1378
+ this.data.agentId
1379
+ );
1360
1380
  this.metricsManager.trackEvent(
1361
1381
  METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
1362
1382
  {
1363
1383
  taskId: this.data.interactionId,
1364
- destination: consultTransferPayload.to,
1365
- destinationType: consultTransferPayload.destinationType,
1384
+ destination: failedDestAgentId || '',
1385
+ destinationType: failedDestinationType,
1366
1386
  isConsultTransfer: true,
1367
1387
  error: error.toString(),
1368
1388
  ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
@@ -276,4 +276,54 @@ describe('Utils', () => {
276
276
  });
277
277
  });
278
278
  });
279
+
280
+ describe('getDestinationAgentId', () => {
281
+ const currentAgentId = 'agent-current-123';
282
+
283
+ it('returns another Agent id when present and not in wrap-up', () => {
284
+ const participants: any = {
285
+ [currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
286
+ agent1: {type: 'Agent', id: 'agent-1', isWrapUp: false},
287
+ customer1: {type: 'Customer', id: 'cust-1', isWrapUp: false},
288
+ };
289
+
290
+ const result = Utils.getDestinationAgentId(participants, currentAgentId);
291
+ expect(result).toBe('agent-1');
292
+ });
293
+
294
+ it('ignores self and wrap-up participants', () => {
295
+ const participants: any = {
296
+ [currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
297
+ agentWrap: {type: 'Agent', id: 'agent-wrap', isWrapUp: true},
298
+ };
299
+
300
+ const result = Utils.getDestinationAgentId(participants, currentAgentId);
301
+ expect(result).toBe('');
302
+ });
303
+
304
+ it('supports DN, EpDn and entryPoint types', () => {
305
+ const participantsDN: any = {
306
+ [currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
307
+ dn1: {type: 'DN', id: 'dn-1', isWrapUp: false},
308
+ };
309
+ expect(Utils.getDestinationAgentId(participantsDN, currentAgentId)).toBe('dn-1');
310
+
311
+ const participantsEpDn: any = {
312
+ [currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
313
+ epdn1: {type: 'EpDn', id: 'epdn-1', isWrapUp: false},
314
+ };
315
+ expect(Utils.getDestinationAgentId(participantsEpDn, currentAgentId)).toBe('epdn-1');
316
+
317
+ const participantsEntry: any = {
318
+ [currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
319
+ entry1: {type: 'entryPoint', id: 'entry-1', isWrapUp: false},
320
+ };
321
+ expect(Utils.getDestinationAgentId(participantsEntry, currentAgentId)).toBe('entry-1');
322
+ });
323
+
324
+ it('returns empty string when participants is missing or empty', () => {
325
+ expect(Utils.getDestinationAgentId(undefined as any, currentAgentId)).toBe('');
326
+ expect(Utils.getDestinationAgentId({} as any, currentAgentId)).toBe('');
327
+ });
328
+ });
279
329
  });
@@ -658,6 +658,13 @@ describe('TaskManager', () => {
658
658
  });
659
659
 
660
660
  it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
661
+ const initialConsultingPayload = {
662
+ data: {
663
+ ...initalPayload.data,
664
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
665
+ },
666
+ };
667
+
661
668
  const consultingPayload = {
662
669
  data: {
663
670
  ...initalPayload.data,
@@ -672,8 +679,8 @@ describe('TaskManager', () => {
672
679
  });
673
680
 
674
681
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
682
+ webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
675
683
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
676
- expect(taskManager.getTask(taskId).updateTaskData).toHaveBeenCalledWith(consultingPayload.data);
677
684
  expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
678
685
  expect(taskEmitSpy).toHaveBeenCalledWith(
679
686
  TASK_EVENTS.TASK_CONSULT_ACCEPTED,
@@ -39,6 +39,7 @@ describe('Task', () => {
39
39
  let loggerInfoSpy;
40
40
  let loggerLogSpy;
41
41
  let loggerErrorSpy;
42
+ let getDestinationAgentIdSpy;
42
43
 
43
44
  const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
44
45
  const mockTrack = {} as MediaStreamTrack;
@@ -141,6 +142,11 @@ describe('Task', () => {
141
142
  },
142
143
  };
143
144
 
145
+ // Mock destination agent id resolution from participants
146
+ getDestinationAgentIdSpy = jest
147
+ .spyOn(Utils, 'getDestinationAgentId')
148
+ .mockReturnValue(taskDataMock.destAgentId);
149
+
144
150
  // Create an instance of Task
145
151
  task = new Task(contactMock, webCallingService, taskDataMock);
146
152
 
@@ -163,6 +169,7 @@ describe('Task', () => {
163
169
 
164
170
  afterEach(() => {
165
171
  jest.clearAllMocks();
172
+ jest.restoreAllMocks();
166
173
  });
167
174
 
168
175
  it('test the on spy', async () => {
@@ -754,29 +761,81 @@ describe('Task', () => {
754
761
  expect(contactMock.consult).toHaveBeenCalledWith({interactionId: taskId, data: consultPayload});
755
762
  expect(response).toEqual(expectedResponse);
756
763
 
757
- const consultTransferPayload: ConsultTransferPayLoad = {
758
- to: '1234',
759
- destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
760
- };
761
-
762
- const consultTransferResponse = await task.consultTransfer(consultTransferPayload);
764
+ const consultTransferResponse = await task.consultTransfer();
763
765
  expect(contactMock.consultTransfer).toHaveBeenCalledWith({
764
766
  interactionId: taskId,
765
- data: consultTransferPayload,
767
+ data: {
768
+ to: taskDataMock.destAgentId,
769
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
770
+ },
766
771
  });
767
772
  expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
768
773
  2,
769
774
  METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
770
775
  {
771
776
  taskId: taskDataMock.interactionId,
772
- destination: consultTransferPayload.to,
773
- destinationType: consultTransferPayload.destinationType,
777
+ destination: taskDataMock.destAgentId,
778
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
774
779
  isConsultTransfer: true,
775
780
  },
776
781
  ['operational', 'behavioral', 'business']
777
782
  );
778
783
  });
779
784
 
785
+ it('should send DIALNUMBER when task destinationType is DN during consultTransfer', async () => {
786
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
787
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
788
+
789
+ // Ensure task data indicates DN scenario
790
+ task.data.destinationType = 'DN' as unknown as string;
791
+
792
+ await task.consultTransfer();
793
+
794
+ expect(contactMock.consultTransfer).toHaveBeenCalledWith({
795
+ interactionId: taskId,
796
+ data: {
797
+ to: taskDataMock.destAgentId,
798
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER,
799
+ },
800
+ });
801
+ });
802
+
803
+ it('should send ENTRYPOINT when task destinationType is EPDN during consultTransfer', async () => {
804
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
805
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
806
+
807
+ // Ensure task data indicates EP/EPDN scenario
808
+ task.data.destinationType = 'EPDN' as unknown as string;
809
+
810
+ await task.consultTransfer();
811
+
812
+ expect(contactMock.consultTransfer).toHaveBeenCalledWith({
813
+ interactionId: taskId,
814
+ data: {
815
+ to: taskDataMock.destAgentId,
816
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT,
817
+ },
818
+ });
819
+ });
820
+
821
+ it('should keep AGENT when task destinationType is neither DN nor EPDN/ENTRYPOINT', async () => {
822
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
823
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
824
+
825
+ // Ensure task data indicates non-DN and non-EP/EPDN scenario
826
+ task.data.destinationType = 'SOMETHING_ELSE' as unknown as string;
827
+
828
+ await task.consultTransfer();
829
+
830
+ expect(contactMock.consultTransfer).toHaveBeenCalledWith({
831
+ interactionId: taskId,
832
+ data: {
833
+ to: taskDataMock.destAgentId,
834
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
835
+ },
836
+ });
837
+ });
838
+
780
839
  it('should do consult transfer to a queue by using the destAgentId from task data', async () => {
781
840
  const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
782
841
  contactMock.consultTransfer.mockResolvedValue(expectedResponse);
@@ -811,6 +870,9 @@ describe('Task', () => {
811
870
  destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE,
812
871
  };
813
872
 
873
+ // For this negative case, ensure computed destination is empty
874
+ getDestinationAgentIdSpy.mockReturnValueOnce('');
875
+
814
876
  await expect(
815
877
  taskWithoutDestAgentId.consultTransfer(queueConsultTransferPayload)
816
878
  ).rejects.toThrow('Error while performing consultTransfer');
@@ -855,8 +917,8 @@ describe('Task', () => {
855
917
  METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
856
918
  {
857
919
  taskId: taskDataMock.interactionId,
858
- destination: consultTransferPayload.to,
859
- destinationType: consultTransferPayload.destinationType,
920
+ destination: taskDataMock.destAgentId,
921
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
860
922
  isConsultTransfer: true,
861
923
  error: error.toString(),
862
924
  ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details),