@webex/contact-center 3.10.0-next.7 → 3.10.0-next.8

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 (41) hide show
  1. package/dist/cc.js +11 -0
  2. package/dist/cc.js.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/services/config/types.js +2 -2
  5. package/dist/services/config/types.js.map +1 -1
  6. package/dist/services/core/Utils.js +90 -71
  7. package/dist/services/core/Utils.js.map +1 -1
  8. package/dist/services/core/constants.js +17 -1
  9. package/dist/services/core/constants.js.map +1 -1
  10. package/dist/services/task/TaskManager.js +61 -36
  11. package/dist/services/task/TaskManager.js.map +1 -1
  12. package/dist/services/task/TaskUtils.js +33 -5
  13. package/dist/services/task/TaskUtils.js.map +1 -1
  14. package/dist/services/task/index.js +49 -58
  15. package/dist/services/task/index.js.map +1 -1
  16. package/dist/services/task/types.js +2 -4
  17. package/dist/services/task/types.js.map +1 -1
  18. package/dist/types/cc.d.ts +6 -0
  19. package/dist/types/index.d.ts +1 -1
  20. package/dist/types/services/config/types.d.ts +4 -4
  21. package/dist/types/services/core/Utils.d.ts +32 -17
  22. package/dist/types/services/core/constants.d.ts +14 -0
  23. package/dist/types/services/task/TaskUtils.d.ts +17 -3
  24. package/dist/types/services/task/types.d.ts +25 -13
  25. package/dist/webex.js +1 -1
  26. package/package.json +1 -1
  27. package/src/cc.ts +11 -0
  28. package/src/index.ts +1 -0
  29. package/src/services/config/types.ts +2 -2
  30. package/src/services/core/Utils.ts +101 -85
  31. package/src/services/core/constants.ts +16 -0
  32. package/src/services/task/TaskManager.ts +75 -28
  33. package/src/services/task/TaskUtils.ts +37 -5
  34. package/src/services/task/index.ts +54 -89
  35. package/src/services/task/types.ts +26 -13
  36. package/test/unit/spec/services/core/Utils.ts +262 -31
  37. package/test/unit/spec/services/task/TaskManager.ts +224 -1
  38. package/test/unit/spec/services/task/TaskUtils.ts +6 -6
  39. package/test/unit/spec/services/task/index.ts +283 -86
  40. package/umd/contact-center.min.js +2 -2
  41. package/umd/contact-center.min.js.map +1 -1
@@ -6,11 +6,10 @@ import WebexRequest from './WebexRequest';
6
6
  import {
7
7
  TaskData,
8
8
  ConsultTransferPayLoad,
9
- ConsultConferenceData,
10
- consultConferencePayloadData,
11
9
  CONSULT_TRANSFER_DESTINATION_TYPE,
12
10
  Interaction,
13
11
  } from '../task/types';
12
+ import {PARTICIPANT_TYPES, STATE_CONSULT} from './constants';
14
13
 
15
14
  /**
16
15
  * Extracts common error details from a Webex request payload.
@@ -218,59 +217,118 @@ export const createErrDetailsObject = (errObj: WebexRequestPayload) => {
218
217
  };
219
218
 
220
219
  /**
221
- * Derives the consult transfer destination type based on the provided task data.
220
+ * Gets the consulted agent ID from the media object by finding the agent
221
+ * in the consult media participants (excluding the current agent).
222
222
  *
223
- * Logic parity with desktop behavior:
224
- * - If agent action is dialing a number (DN/EPDN/ENTRYPOINT):
225
- * - ENTRYPOINT/EPDN map to ENTRYPOINT
226
- * - DN maps to DIALNUMBER
227
- * - Otherwise defaults to AGENT
223
+ * @param media - The media object from the interaction
224
+ * @param agentId - The current agent's ID to exclude from the search
225
+ * @returns The consulted agent ID, or empty string if none found
226
+ */
227
+ export const getConsultedAgentId = (media: Interaction['media'], agentId: string): string => {
228
+ let consultParticipants: string[] = [];
229
+ let consultedParticipantId = '';
230
+
231
+ Object.keys(media).forEach((key) => {
232
+ if (media[key].mType === STATE_CONSULT) {
233
+ consultParticipants = media[key].participants;
234
+ }
235
+ });
236
+
237
+ if (consultParticipants.includes(agentId)) {
238
+ const id = consultParticipants.find((participant) => participant !== agentId);
239
+ consultedParticipantId = id || consultedParticipantId;
240
+ }
241
+
242
+ return consultedParticipantId;
243
+ };
244
+
245
+ /**
246
+ * Gets the destination agent ID for CBT (Capacity Based Team) scenarios.
247
+ * CBT refers to teams created in Control Hub with capacity-based routing
248
+ * (as opposed to agent-based routing). This handles cases where the consulted
249
+ * participant is not directly in participants but can be found by matching
250
+ * the dial number (dn).
228
251
  *
229
- * @param taskData - The task data used to infer the agent action and destination type
230
- * @returns The normalized destination type to be used for consult transfer
252
+ * @param interaction - The interaction object
253
+ * @param consultingAgent - The consulting agent identifier
254
+ * @returns The destination agent ID for CBT scenarios, or empty string if none found
231
255
  */
256
+ export const getDestAgentIdForCBT = (interaction: Interaction, consultingAgent: string): string => {
257
+ const participants = interaction.participants;
258
+ let destAgentIdForCBT = '';
259
+
260
+ // Check if this is a CBT scenario (consultingAgent exists but not directly in participants)
261
+ if (consultingAgent && !participants[consultingAgent]) {
262
+ const foundEntry = Object.entries(participants).find(
263
+ ([, participant]: [string, Interaction['participants'][string]]) => {
264
+ return (
265
+ participant.pType.toLowerCase() === PARTICIPANT_TYPES.DN &&
266
+ participant.type === PARTICIPANT_TYPES.AGENT &&
267
+ participant.dn === consultingAgent
268
+ );
269
+ }
270
+ );
271
+
272
+ if (foundEntry) {
273
+ destAgentIdForCBT = foundEntry[0];
274
+ }
275
+ }
276
+
277
+ return destAgentIdForCBT;
278
+ };
279
+
232
280
  /**
233
- * Checks if a participant type represents a non-customer participant.
234
- * Non-customer participants include agents, dial numbers, entry point dial numbers,
235
- * and entry points.
281
+ * Calculates the destination agent ID for consult operations.
282
+ *
283
+ * @param interaction - The interaction object
284
+ * @param agentId - The current agent's ID
285
+ * @returns The destination agent ID
236
286
  */
237
- const isNonCustomerParticipant = (participantType: string): boolean => {
238
- return (
239
- participantType === 'Agent' ||
240
- participantType === 'DN' ||
241
- participantType === 'EpDn' ||
242
- participantType === 'entryPoint'
243
- );
287
+ export const calculateDestAgentId = (interaction: Interaction, agentId: string): string => {
288
+ const consultingAgent = getConsultedAgentId(interaction.media, agentId);
289
+
290
+ // Check if this is a CBT (Capacity Based Team) scenario
291
+ // If not CBT, the function will return empty string and we'll use the normal flow
292
+ const destAgentIdCBT = getDestAgentIdForCBT(interaction, consultingAgent);
293
+ if (destAgentIdCBT) {
294
+ return destAgentIdCBT;
295
+ }
296
+
297
+ return interaction.participants[consultingAgent]?.type === PARTICIPANT_TYPES.EP_DN
298
+ ? interaction.participants[consultingAgent]?.epId
299
+ : interaction.participants[consultingAgent]?.id;
244
300
  };
245
301
 
246
302
  /**
247
- * Gets the destination agent ID from participants data by finding the first
248
- * non-customer participant that is not the current agent and is not in wrap-up state.
303
+ * Calculates the destination agent ID for fetching destination type.
249
304
  *
250
- * @param participants - The participants data from the interaction
251
- * @param agentId - The current agent's ID to exclude from the search
252
- * @returns The destination agent ID, or empty string if none found
305
+ * @param interaction - The interaction object
306
+ * @param agentId - The current agent's ID
307
+ * @returns The destination agent ID for determining destination type
253
308
  */
254
- export const getDestinationAgentId = (
255
- participants: Interaction['participants'],
256
- agentId: string
257
- ): string => {
258
- let id = '';
259
-
260
- if (participants) {
261
- Object.keys(participants).forEach((participant) => {
262
- const participantData = participants[participant];
263
- if (
264
- isNonCustomerParticipant(participantData.type) &&
265
- participantData.id !== agentId &&
266
- !participantData.isWrapUp
267
- ) {
268
- id = participantData.id;
269
- }
270
- });
309
+ export const calculateDestType = (interaction: Interaction, agentId: string): string => {
310
+ const consultingAgent = getConsultedAgentId(interaction.media, agentId);
311
+
312
+ // Check if this is a CBT (Capacity Based Team) scenario, otherwise use consultingAgent
313
+ const destAgentIdCBT = getDestAgentIdForCBT(interaction, consultingAgent);
314
+ const destinationaegntId = destAgentIdCBT || consultingAgent;
315
+ const destAgentType = destinationaegntId
316
+ ? interaction.participants[destinationaegntId]?.pType
317
+ : undefined;
318
+ if (destAgentType) {
319
+ if (destAgentType === 'DN') {
320
+ return CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
321
+ }
322
+ if (destAgentType === 'EP-DN') {
323
+ return CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT;
324
+ }
325
+ // Keep the existing destinationType if it's something else (like "agent" or "Agent")
326
+ // Convert "Agent" to lowercase for consistency
327
+
328
+ return destAgentType.toLowerCase();
271
329
  }
272
330
 
273
- return id;
331
+ return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
274
332
  };
275
333
 
276
334
  export const deriveConsultTransferDestinationType = (
@@ -286,45 +344,3 @@ export const deriveConsultTransferDestinationType = (
286
344
 
287
345
  return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
288
346
  };
289
-
290
- /**
291
- * Builds consult conference parameter data using EXACT Agent Desktop logic.
292
- * This matches the Agent Desktop's consultConference implementation exactly.
293
- *
294
- * @param dataPassed - Original consultation data from Agent Desktop format
295
- * @param interactionIdPassed - The interaction ID for the task
296
- * @returns Object with interactionId and ConsultConferenceData matching Agent Desktop format
297
- * @public
298
- */
299
- export const buildConsultConferenceParamData = (
300
- dataPassed: consultConferencePayloadData,
301
- interactionIdPassed: string
302
- ): {interactionId: string; data: ConsultConferenceData} => {
303
- const data: ConsultConferenceData = {
304
- // Include agentId if present in input data
305
- ...('agentId' in dataPassed && {agentId: dataPassed.agentId}),
306
- // Handle destAgentId from consultation data
307
- to: dataPassed.destAgentId,
308
- destinationType: '',
309
- };
310
-
311
- // Agent Desktop destination type logic
312
- if ('destinationType' in dataPassed) {
313
- if (dataPassed.destinationType === 'DN') {
314
- data.destinationType = CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
315
- } else if (dataPassed.destinationType === 'EP_DN') {
316
- data.destinationType = CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT;
317
- } else {
318
- // Keep the existing destinationType if it's something else (like "agent" or "Agent")
319
- // Convert "Agent" to lowercase for consistency
320
- data.destinationType = dataPassed.destinationType.toLowerCase();
321
- }
322
- } else {
323
- data.destinationType = CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
324
- }
325
-
326
- return {
327
- interactionId: interactionIdPassed,
328
- data,
329
- };
330
- };
@@ -64,6 +64,22 @@ export const CONNECTIVITY_CHECK_INTERVAL = 5000;
64
64
  */
65
65
  export const CLOSE_SOCKET_TIMEOUT = 16000;
66
66
 
67
+ /**
68
+ * Constants for participant types, destination types, and interaction states
69
+ * @ignore
70
+ */
71
+ export const PARTICIPANT_TYPES = {
72
+ /** Participant type for Entry Point Dial Number */
73
+ EP_DN: 'EpDn',
74
+ /** Participant type for dial number */
75
+ DN: 'dn',
76
+ /** Participant type for Agent */
77
+ AGENT: 'Agent',
78
+ };
79
+
80
+ /** Interaction state for consultation */
81
+ export const STATE_CONSULT = 'consult';
82
+
67
83
  // Method names for core services
68
84
  export const METHODS = {
69
85
  // WebexRequest methods
@@ -17,6 +17,7 @@ import {
17
17
  getIsConferenceInProgress,
18
18
  isParticipantInMainInteraction,
19
19
  isPrimary,
20
+ isSecondaryEpDnAgent,
20
21
  } from './TaskUtils';
21
22
 
22
23
  /** @internal */
@@ -129,11 +130,6 @@ export default class TaskManager extends EventEmitter {
129
130
  interactionId: payload.data.interactionId,
130
131
  });
131
132
 
132
- // Pre-calculate isConferenceInProgress for the initial task data
133
- const simulatedTaskForAgentContact = {
134
- data: {...payload.data},
135
- } as ITask;
136
-
137
133
  task = new Task(
138
134
  this.contact,
139
135
  this.webCallingService,
@@ -141,7 +137,7 @@ export default class TaskManager extends EventEmitter {
141
137
  ...payload.data,
142
138
  wrapUpRequired:
143
139
  payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
144
- isConferenceInProgress: getIsConferenceInProgress(simulatedTaskForAgentContact),
140
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
145
141
  },
146
142
  this.wrapupData,
147
143
  this.agentId
@@ -252,13 +248,22 @@ export default class TaskManager extends EventEmitter {
252
248
  break;
253
249
  }
254
250
  case CC_EVENTS.CONTACT_ENDED:
251
+ // Update task data
255
252
  task = this.updateTaskData(task, {
256
253
  ...payload.data,
257
- wrapUpRequired: payload.data.interaction.state !== 'new',
254
+ wrapUpRequired:
255
+ payload.data.interaction.state !== 'new' &&
256
+ !isSecondaryEpDnAgent(payload.data.interaction),
258
257
  });
258
+
259
+ // Handle cleanup based on whether task should be deleted
259
260
  this.handleTaskCleanup(task);
260
- task.emit(TASK_EVENTS.TASK_END, task);
261
+
262
+ task?.emit(TASK_EVENTS.TASK_END, task);
261
263
 
264
+ break;
265
+ case CC_EVENTS.CONTACT_MERGED:
266
+ task = this.handleContactMerged(task, payload.data);
262
267
  break;
263
268
  case CC_EVENTS.AGENT_CONTACT_HELD:
264
269
  // As soon as the main interaction is held, we need to emit TASK_HOLD
@@ -380,32 +385,22 @@ export default class TaskManager extends EventEmitter {
380
385
  } else {
381
386
  this.removeTaskFromCollection(task);
382
387
  }
383
- task?.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
388
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
384
389
  break;
385
390
  case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
386
- // Participant joined conference - update task state with participant information and emit event
387
- // Pre-calculate isConferenceInProgress with updated data to avoid double update
388
- const simulatedTaskForJoin = {
389
- ...task,
390
- data: {...task.data, ...payload.data},
391
- };
392
391
  task = this.updateTaskData(task, {
393
392
  ...payload.data,
394
- isConferenceInProgress: getIsConferenceInProgress(simulatedTaskForJoin),
393
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
395
394
  });
396
395
  task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
397
396
  break;
398
397
  }
399
398
  case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
400
399
  // Conference ended - update task state and emit event
401
- // Pre-calculate isConferenceInProgress with updated data to avoid double update
402
- const simulatedTaskForLeft = {
403
- ...task,
404
- data: {...task.data, ...payload.data},
405
- };
400
+
406
401
  task = this.updateTaskData(task, {
407
402
  ...payload.data,
408
- isConferenceInProgress: getIsConferenceInProgress(simulatedTaskForLeft),
403
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
409
404
  });
410
405
  if (checkParticipantNotInInteraction(task, this.agentId)) {
411
406
  if (
@@ -441,13 +436,10 @@ export default class TaskManager extends EventEmitter {
441
436
  task = this.updateTaskData(task, payload.data);
442
437
  task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
443
438
  break;
444
- case CC_EVENTS.CONSULTED_PARTICIPANT_MOVING:
445
- // Participant is being moved/transferred - update task state with movement info
446
- task = this.updateTaskData(task, payload.data);
447
- break;
448
439
  case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
449
440
  // Post-call activity for participant - update task state with activity details
450
441
  task = this.updateTaskData(task, payload.data);
442
+ task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
451
443
  break;
452
444
  default:
453
445
  break;
@@ -487,6 +479,54 @@ export default class TaskManager extends EventEmitter {
487
479
  }
488
480
  }
489
481
 
482
+ /**
483
+ * Handles CONTACT_MERGED event logic
484
+ * @param task - The task to process
485
+ * @param taskData - The task data from the event payload
486
+ * @returns Updated or newly created task
487
+ * @private
488
+ */
489
+ private handleContactMerged(task: ITask, taskData: TaskData): ITask {
490
+ if (taskData.childInteractionId) {
491
+ // remove the child task from collection
492
+ this.removeTaskFromCollection(this.taskCollection[taskData.childInteractionId]);
493
+ }
494
+
495
+ if (this.taskCollection[taskData.interactionId]) {
496
+ LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
497
+ module: TASK_MANAGER_FILE,
498
+ method: METHODS.REGISTER_TASK_LISTENERS,
499
+ interactionId: taskData.interactionId,
500
+ });
501
+ // update the task data
502
+ task = this.updateTaskData(task, taskData);
503
+ } else {
504
+ // Case2 : Task is not present in taskCollection
505
+ LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
506
+ module: TASK_MANAGER_FILE,
507
+ method: METHODS.REGISTER_TASK_LISTENERS,
508
+ interactionId: taskData.interactionId,
509
+ });
510
+
511
+ task = new Task(
512
+ this.contact,
513
+ this.webCallingService,
514
+ {
515
+ ...taskData,
516
+ wrapUpRequired: taskData.interaction?.participants?.[this.agentId]?.isWrapUp || false,
517
+ isConferenceInProgress: getIsConferenceInProgress(taskData),
518
+ },
519
+ this.wrapupData,
520
+ this.agentId
521
+ );
522
+ this.taskCollection[taskData.interactionId] = task;
523
+ }
524
+
525
+ this.emit(TASK_EVENTS.TASK_MERGED, task);
526
+
527
+ return task;
528
+ }
529
+
490
530
  private removeTaskFromCollection(task: ITask) {
491
531
  if (task?.data?.interactionId) {
492
532
  delete this.taskCollection[task.data.interactionId];
@@ -498,7 +538,13 @@ export default class TaskManager extends EventEmitter {
498
538
  }
499
539
  }
500
540
 
541
+ /**
542
+ * Handles cleanup of task resources including Desktop/WebRTC call cleanup and task removal
543
+ * @param task - The task to clean up
544
+ * @private
545
+ */
501
546
  private handleTaskCleanup(task: ITask) {
547
+ // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
502
548
  if (
503
549
  this.webCallingService.loginOption === LoginOption.BROWSER &&
504
550
  task.data.interaction.mediaType === 'telephony'
@@ -506,8 +552,9 @@ export default class TaskManager extends EventEmitter {
506
552
  task.unregisterWebCallListeners();
507
553
  this.webCallingService.cleanUpCall();
508
554
  }
509
- if (task.data.interaction.state === 'new') {
510
- // Only remove tasks in 'new' state immediately. For other states,
555
+
556
+ if (task.data.interaction.state === 'new' || isSecondaryEpDnAgent(task.data.interaction)) {
557
+ // Only remove tasks in 'new' state or isSecondaryEpDnAgent immediately. For other states,
511
558
  // retain tasks until they complete wrap-up, unless the task disconnected before being answered.
512
559
  this.removeTaskFromCollection(task);
513
560
  }
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
- import {ITask} from './types';
2
+ import {Interaction, ITask, TaskData} from './types';
3
3
 
4
4
  /**
5
5
  * Determines if the given agent is the primary agent (owner) of the task
@@ -53,13 +53,13 @@ export const checkParticipantNotInInteraction = (task: ITask, agentId: string):
53
53
 
54
54
  /**
55
55
  * Determines if a conference is currently in progress based on the number of active agent participants
56
- * @param task - The task to check for conference status
56
+ * @param TaskData - The payLoad data to check for conference status
57
57
  * @returns true if there are 2 or more active agent participants in the main call, false otherwise
58
58
  */
59
- export const getIsConferenceInProgress = (task: ITask): boolean => {
60
- const mediaMainCall = task?.data?.interaction?.media?.[task?.data?.interactionId];
59
+ export const getIsConferenceInProgress = (data: TaskData): boolean => {
60
+ const mediaMainCall = data.interaction.media?.[data?.interactionId];
61
61
  const participantsInMainCall = new Set(mediaMainCall?.participants);
62
- const participants = task?.data?.interaction?.participants;
62
+ const participants = data.interaction.participants;
63
63
 
64
64
  const agentParticipants = new Set();
65
65
  if (participantsInMainCall.size > 0) {
@@ -79,3 +79,35 @@ export const getIsConferenceInProgress = (task: ITask): boolean => {
79
79
 
80
80
  return agentParticipants.size >= 2;
81
81
  };
82
+
83
+ /**
84
+ * Checks if the current agent is a secondary agent in a consultation scenario.
85
+ * Secondary agents are those who were consulted (not the original call owner).
86
+ * @param task - The task object containing interaction details
87
+ * @returns true if this is a secondary agent (consulted party), false otherwise
88
+ */
89
+ export const isSecondaryAgent = (interaction: Interaction): boolean => {
90
+ if (!interaction.callProcessingDetails) {
91
+ return false;
92
+ }
93
+
94
+ return (
95
+ interaction.callProcessingDetails.relationshipType === 'consult' &&
96
+ !!interaction.callProcessingDetails.parentInteractionId &&
97
+ interaction.callProcessingDetails.parentInteractionId !== interaction.interactionId
98
+ );
99
+ };
100
+
101
+ /**
102
+ * Checks if the current agent is a secondary EP-DN (Entry Point Dial Number) agent.
103
+ * This is specifically for telephony consultations to external numbers/entry points.
104
+ * @param task - The task object containing interaction details
105
+ * @returns true if this is a secondary EP-DN agent in telephony consultation, false otherwise
106
+ */
107
+ export const isSecondaryEpDnAgent = (interaction: Interaction): boolean => {
108
+ if (!interaction) {
109
+ return false;
110
+ }
111
+
112
+ return interaction.mediaType === 'telephony' && isSecondaryAgent(interaction);
113
+ };