@webex/contact-center 3.9.0-next.9 → 3.10.0-multi-llms.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/cc.js +193 -47
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +1 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +9 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/logger-proxy.js +24 -1
  8. package/dist/logger-proxy.js.map +1 -1
  9. package/dist/metrics/behavioral-events.js +89 -0
  10. package/dist/metrics/behavioral-events.js.map +1 -1
  11. package/dist/metrics/constants.js +30 -2
  12. package/dist/metrics/constants.js.map +1 -1
  13. package/dist/services/AddressBook.js +271 -0
  14. package/dist/services/AddressBook.js.map +1 -0
  15. package/dist/services/EntryPoint.js +227 -0
  16. package/dist/services/EntryPoint.js.map +1 -0
  17. package/dist/services/Queue.js +261 -0
  18. package/dist/services/Queue.js.map +1 -0
  19. package/dist/services/config/constants.js +36 -2
  20. package/dist/services/config/constants.js.map +1 -1
  21. package/dist/services/config/index.js +29 -21
  22. package/dist/services/config/index.js.map +1 -1
  23. package/dist/services/config/types.js +33 -1
  24. package/dist/services/config/types.js.map +1 -1
  25. package/dist/services/core/Utils.js +91 -31
  26. package/dist/services/core/Utils.js.map +1 -1
  27. package/dist/services/core/constants.js +17 -1
  28. package/dist/services/core/constants.js.map +1 -1
  29. package/dist/services/task/TaskManager.js +150 -7
  30. package/dist/services/task/TaskManager.js.map +1 -1
  31. package/dist/services/task/TaskUtils.js +104 -0
  32. package/dist/services/task/TaskUtils.js.map +1 -0
  33. package/dist/services/task/constants.js +26 -1
  34. package/dist/services/task/constants.js.map +1 -1
  35. package/dist/services/task/contact.js +86 -0
  36. package/dist/services/task/contact.js.map +1 -1
  37. package/dist/services/task/index.js +302 -39
  38. package/dist/services/task/index.js.map +1 -1
  39. package/dist/services/task/types.js +12 -0
  40. package/dist/services/task/types.js.map +1 -1
  41. package/dist/types/cc.d.ts +121 -35
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/index.d.ts +4 -3
  44. package/dist/types/metrics/constants.d.ts +24 -1
  45. package/dist/types/services/AddressBook.d.ts +74 -0
  46. package/dist/types/services/EntryPoint.d.ts +67 -0
  47. package/dist/types/services/Queue.d.ts +76 -0
  48. package/dist/types/services/config/constants.d.ts +35 -1
  49. package/dist/types/services/config/index.d.ts +6 -9
  50. package/dist/types/services/config/types.d.ts +79 -58
  51. package/dist/types/services/core/Utils.d.ts +33 -5
  52. package/dist/types/services/core/constants.d.ts +14 -0
  53. package/dist/types/services/task/TaskUtils.d.ts +42 -0
  54. package/dist/types/services/task/constants.d.ts +23 -0
  55. package/dist/types/services/task/contact.d.ts +10 -0
  56. package/dist/types/services/task/index.d.ts +84 -3
  57. package/dist/types/services/task/types.d.ts +245 -21
  58. package/dist/types/types.d.ts +162 -0
  59. package/dist/types/utils/PageCache.d.ts +173 -0
  60. package/dist/types.js +17 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/utils/PageCache.js +192 -0
  63. package/dist/utils/PageCache.js.map +1 -0
  64. package/dist/webex.js +1 -1
  65. package/package.json +10 -9
  66. package/src/cc.ts +217 -52
  67. package/src/constants.ts +1 -0
  68. package/src/index.ts +17 -2
  69. package/src/logger-proxy.ts +24 -1
  70. package/src/metrics/behavioral-events.ts +94 -0
  71. package/src/metrics/constants.ts +34 -1
  72. package/src/services/AddressBook.ts +291 -0
  73. package/src/services/EntryPoint.ts +241 -0
  74. package/src/services/Queue.ts +277 -0
  75. package/src/services/config/constants.ts +42 -2
  76. package/src/services/config/index.ts +30 -30
  77. package/src/services/config/types.ts +59 -58
  78. package/src/services/core/Utils.ts +101 -41
  79. package/src/services/core/constants.ts +16 -0
  80. package/src/services/task/TaskManager.ts +181 -9
  81. package/src/services/task/TaskUtils.ts +113 -0
  82. package/src/services/task/constants.ts +25 -0
  83. package/src/services/task/contact.ts +80 -0
  84. package/src/services/task/index.ts +364 -54
  85. package/src/services/task/types.ts +264 -20
  86. package/src/types.ts +180 -0
  87. package/src/utils/PageCache.ts +252 -0
  88. package/test/unit/spec/cc.ts +282 -85
  89. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  90. package/test/unit/spec/services/AddressBook.ts +332 -0
  91. package/test/unit/spec/services/EntryPoint.ts +259 -0
  92. package/test/unit/spec/services/Queue.ts +323 -0
  93. package/test/unit/spec/services/config/index.ts +279 -65
  94. package/test/unit/spec/services/core/Utils.ts +262 -31
  95. package/test/unit/spec/services/task/TaskManager.ts +752 -1
  96. package/test/unit/spec/services/task/TaskUtils.ts +131 -0
  97. package/test/unit/spec/services/task/contact.ts +31 -1
  98. package/test/unit/spec/services/task/index.ts +675 -69
  99. package/umd/contact-center.min.js +2 -2
  100. package/umd/contact-center.min.js.map +1 -1
@@ -9,6 +9,7 @@ import {
9
9
  CONSULT_TRANSFER_DESTINATION_TYPE,
10
10
  Interaction,
11
11
  } from '../task/types';
12
+ import {PARTICIPANT_TYPES, STATE_CONSULT} from './constants';
12
13
 
13
14
  /**
14
15
  * Extracts common error details from a Webex request payload.
@@ -216,59 +217,118 @@ export const createErrDetailsObject = (errObj: WebexRequestPayload) => {
216
217
  };
217
218
 
218
219
  /**
219
- * 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).
220
222
  *
221
- * Logic parity with desktop behavior:
222
- * - If agent action is dialing a number (DN/EPDN/ENTRYPOINT):
223
- * - ENTRYPOINT/EPDN map to ENTRYPOINT
224
- * - DN maps to DIALNUMBER
225
- * - 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).
226
251
  *
227
- * @param taskData - The task data used to infer the agent action and destination type
228
- * @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
229
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
+
230
280
  /**
231
- * Checks if a participant type represents a non-customer participant.
232
- * Non-customer participants include agents, dial numbers, entry point dial numbers,
233
- * 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
234
286
  */
235
- const isNonCustomerParticipant = (participantType: string): boolean => {
236
- return (
237
- participantType === 'Agent' ||
238
- participantType === 'DN' ||
239
- participantType === 'EpDn' ||
240
- participantType === 'entryPoint'
241
- );
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;
242
300
  };
243
301
 
244
302
  /**
245
- * Gets the destination agent ID from participants data by finding the first
246
- * 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.
247
304
  *
248
- * @param participants - The participants data from the interaction
249
- * @param agentId - The current agent's ID to exclude from the search
250
- * @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
251
308
  */
252
- export const getDestinationAgentId = (
253
- participants: Interaction['participants'],
254
- agentId: string
255
- ): string => {
256
- let id = '';
257
-
258
- if (participants) {
259
- Object.keys(participants).forEach((participant) => {
260
- const participantData = participants[participant];
261
- if (
262
- isNonCustomerParticipant(participantData.type) &&
263
- participantData.id !== agentId &&
264
- !participantData.isWrapUp
265
- ) {
266
- id = participantData.id;
267
- }
268
- });
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();
269
329
  }
270
330
 
271
- return id;
331
+ return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
272
332
  };
273
333
 
274
334
  export const deriveConsultTransferDestinationType = (
@@ -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
@@ -12,6 +12,13 @@ import LoggerProxy from '../../logger-proxy';
12
12
  import Task from '.';
13
13
  import MetricsManager from '../../metrics/MetricsManager';
14
14
  import {METRIC_EVENT_NAMES} from '../../metrics/constants';
15
+ import {
16
+ checkParticipantNotInInteraction,
17
+ getIsConferenceInProgress,
18
+ isParticipantInMainInteraction,
19
+ isPrimary,
20
+ isSecondaryEpDnAgent,
21
+ } from './TaskUtils';
15
22
 
16
23
  /** @internal */
17
24
  export default class TaskManager extends EventEmitter {
@@ -28,6 +35,7 @@ export default class TaskManager extends EventEmitter {
28
35
  private metricsManager: MetricsManager;
29
36
  private static taskManager;
30
37
  private wrapupData: WrapupData;
38
+ private agentId: string;
31
39
  /**
32
40
  * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
33
41
  * @param webCallingService - Webrtc Service Layer
@@ -52,6 +60,19 @@ export default class TaskManager extends EventEmitter {
52
60
  this.wrapupData = wrapupData;
53
61
  }
54
62
 
63
+ public setAgentId(agentId: string) {
64
+ this.agentId = agentId;
65
+ }
66
+
67
+ /**
68
+ * Gets the current agent ID
69
+ * @returns {string} The agent ID set for this task manager instance
70
+ * @public
71
+ */
72
+ public getAgentId(): string {
73
+ return this.agentId;
74
+ }
75
+
55
76
  private handleIncomingWebCall = (call: ICall) => {
56
77
  const currentTask = Object.values(this.taskCollection).find(
57
78
  (task) => task.data.interaction.mediaType === 'telephony'
@@ -108,16 +129,18 @@ export default class TaskManager extends EventEmitter {
108
129
  method: METHODS.REGISTER_TASK_LISTENERS,
109
130
  interactionId: payload.data.interactionId,
110
131
  });
132
+
111
133
  task = new Task(
112
134
  this.contact,
113
135
  this.webCallingService,
114
136
  {
115
137
  ...payload.data,
116
138
  wrapUpRequired:
117
- payload.data.interaction?.participants?.[payload.data.agentId]?.isWrapUp ||
118
- false,
139
+ payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
140
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
119
141
  },
120
- this.wrapupData
142
+ this.wrapupData,
143
+ this.agentId
121
144
  );
122
145
  this.taskCollection[payload.data.interactionId] = task;
123
146
  // Condition 1: The state is=new i.e it is a incoming task
@@ -145,6 +168,7 @@ export default class TaskManager extends EventEmitter {
145
168
  }
146
169
  }
147
170
  break;
171
+
148
172
  case CC_EVENTS.AGENT_CONTACT_RESERVED:
149
173
  task = new Task(
150
174
  this.contact,
@@ -153,8 +177,9 @@ export default class TaskManager extends EventEmitter {
153
177
  ...payload.data,
154
178
  isConsulted: false,
155
179
  },
156
- this.wrapupData
157
- ); // Ensure isConsulted prop exists
180
+ this.wrapupData,
181
+ this.agentId
182
+ );
158
183
  this.taskCollection[payload.data.interactionId] = task;
159
184
  if (
160
185
  this.webCallingService.loginOption !== LoginOption.BROWSER ||
@@ -223,13 +248,22 @@ export default class TaskManager extends EventEmitter {
223
248
  break;
224
249
  }
225
250
  case CC_EVENTS.CONTACT_ENDED:
251
+ // Update task data
226
252
  task = this.updateTaskData(task, {
227
253
  ...payload.data,
228
- wrapUpRequired: payload.data.interaction.state !== 'new',
254
+ wrapUpRequired:
255
+ payload.data.interaction.state !== 'new' &&
256
+ !isSecondaryEpDnAgent(payload.data.interaction),
229
257
  });
258
+
259
+ // Handle cleanup based on whether task should be deleted
230
260
  this.handleTaskCleanup(task);
231
- task.emit(TASK_EVENTS.TASK_END, task);
261
+
262
+ task?.emit(TASK_EVENTS.TASK_END, task);
232
263
 
264
+ break;
265
+ case CC_EVENTS.CONTACT_MERGED:
266
+ task = this.handleContactMerged(task, payload.data);
233
267
  break;
234
268
  case CC_EVENTS.AGENT_CONTACT_HELD:
235
269
  // As soon as the main interaction is held, we need to emit TASK_HOLD
@@ -324,6 +358,89 @@ export default class TaskManager extends EventEmitter {
324
358
  task = this.updateTaskData(task, payload.data);
325
359
  task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
326
360
  break;
361
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
362
+ // Conference is being established - update task state and emit establishing event
363
+ task = this.updateTaskData(task, payload.data);
364
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
365
+ break;
366
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
367
+ // Conference started successfully - update task state and emit event
368
+ task = this.updateTaskData(task, payload.data);
369
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
370
+ break;
371
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
372
+ // Conference failed - update task state and emit failure event
373
+ task = this.updateTaskData(task, payload.data);
374
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
375
+ break;
376
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
377
+ // Conference ended - update task state and emit event
378
+ task = this.updateTaskData(task, payload.data);
379
+ if (
380
+ !task ||
381
+ isPrimary(task, this.agentId) ||
382
+ isParticipantInMainInteraction(task, this.agentId)
383
+ ) {
384
+ LoggerProxy.log('Primary or main interaction participant leaving conference');
385
+ } else {
386
+ this.removeTaskFromCollection(task);
387
+ }
388
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
389
+ break;
390
+ case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
391
+ task = this.updateTaskData(task, {
392
+ ...payload.data,
393
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
394
+ });
395
+ task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
396
+ break;
397
+ }
398
+ case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
399
+ // Conference ended - update task state and emit event
400
+
401
+ task = this.updateTaskData(task, {
402
+ ...payload.data,
403
+ isConferenceInProgress: getIsConferenceInProgress(payload.data),
404
+ });
405
+ if (checkParticipantNotInInteraction(task, this.agentId)) {
406
+ if (
407
+ isParticipantInMainInteraction(task, this.agentId) ||
408
+ isPrimary(task, this.agentId)
409
+ ) {
410
+ LoggerProxy.log('Primary or main interaction participant leaving conference');
411
+ } else {
412
+ this.removeTaskFromCollection(task);
413
+ }
414
+ }
415
+ task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
416
+ break;
417
+ }
418
+ case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
419
+ // Conference exit failed - update task state and emit failure event
420
+ task = this.updateTaskData(task, payload.data);
421
+ task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
422
+ break;
423
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
424
+ // Conference end failed - update task state with error details and emit failure event
425
+ task = this.updateTaskData(task, payload.data);
426
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
427
+ break;
428
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
429
+ // Conference was transferred - update task state and emit transfer success event
430
+ // Note: Backend should provide hasLeft and wrapUpRequired status
431
+ task = this.updateTaskData(task, payload.data);
432
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
433
+ break;
434
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
435
+ // Conference transfer failed - update task state with error details and emit failure event
436
+ task = this.updateTaskData(task, payload.data);
437
+ task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
438
+ break;
439
+ case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
440
+ // Post-call activity for participant - update task state with activity details
441
+ task = this.updateTaskData(task, payload.data);
442
+ task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
443
+ break;
327
444
  default:
328
445
  break;
329
446
  }
@@ -362,6 +479,54 @@ export default class TaskManager extends EventEmitter {
362
479
  }
363
480
  }
364
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
+
365
530
  private removeTaskFromCollection(task: ITask) {
366
531
  if (task?.data?.interactionId) {
367
532
  delete this.taskCollection[task.data.interactionId];
@@ -373,7 +538,13 @@ export default class TaskManager extends EventEmitter {
373
538
  }
374
539
  }
375
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
+ */
376
546
  private handleTaskCleanup(task: ITask) {
547
+ // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
377
548
  if (
378
549
  this.webCallingService.loginOption === LoginOption.BROWSER &&
379
550
  task.data.interaction.mediaType === 'telephony'
@@ -381,8 +552,9 @@ export default class TaskManager extends EventEmitter {
381
552
  task.unregisterWebCallListeners();
382
553
  this.webCallingService.cleanUpCall();
383
554
  }
384
- if (task.data.interaction.state === 'new') {
385
- // 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,
386
558
  // retain tasks until they complete wrap-up, unless the task disconnected before being answered.
387
559
  this.removeTaskFromCollection(task);
388
560
  }
@@ -0,0 +1,113 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import {Interaction, ITask, TaskData} from './types';
3
+
4
+ /**
5
+ * Determines if the given agent is the primary agent (owner) of the task
6
+ * @param task - The task to check
7
+ * @param agentId - The agent ID to check for primary status
8
+ * @returns true if the agent is the primary agent, false otherwise
9
+ */
10
+ export const isPrimary = (task: ITask, agentId: string): boolean => {
11
+ if (!task.data?.interaction?.owner) {
12
+ // Fall back to checking data.agentId when owner is not set
13
+ return task.data.agentId === agentId;
14
+ }
15
+
16
+ return task.data.interaction.owner === agentId;
17
+ };
18
+
19
+ /**
20
+ * Checks if the given agent is a participant in the main interaction (mainCall)
21
+ * @param task - The task to check
22
+ * @param agentId - The agent ID to check for participation
23
+ * @returns true if the agent is a participant in the main interaction, false otherwise
24
+ */
25
+ export const isParticipantInMainInteraction = (task: ITask, agentId: string): boolean => {
26
+ if (!task?.data?.interaction?.media) {
27
+ return false;
28
+ }
29
+
30
+ return Object.values(task.data.interaction.media).some(
31
+ (mediaObj) =>
32
+ mediaObj && mediaObj.mType === 'mainCall' && mediaObj.participants?.includes(agentId)
33
+ );
34
+ };
35
+
36
+ /**
37
+ * Checks if the given agent is not in the interaction or has left the interaction
38
+ * @param task - The task to check
39
+ * @param agentId - The agent ID to check
40
+ * @returns true if the agent is not in the interaction or has left, false otherwise
41
+ */
42
+ export const checkParticipantNotInInteraction = (task: ITask, agentId: string): boolean => {
43
+ if (!task?.data?.interaction?.participants) {
44
+ return true;
45
+ }
46
+ const {data} = task;
47
+
48
+ return (
49
+ !(agentId in data.interaction.participants) ||
50
+ (agentId in data.interaction.participants && data.interaction.participants[agentId].hasLeft)
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Determines if a conference is currently in progress based on the number of active agent participants
56
+ * @param TaskData - The payLoad data to check for conference status
57
+ * @returns true if there are 2 or more active agent participants in the main call, false otherwise
58
+ */
59
+ export const getIsConferenceInProgress = (data: TaskData): boolean => {
60
+ const mediaMainCall = data.interaction.media?.[data?.interactionId];
61
+ const participantsInMainCall = new Set(mediaMainCall?.participants);
62
+ const participants = data.interaction.participants;
63
+
64
+ const agentParticipants = new Set();
65
+ if (participantsInMainCall.size > 0) {
66
+ participantsInMainCall.forEach((participantId: string) => {
67
+ const participant = participants?.[participantId];
68
+ if (
69
+ participant &&
70
+ participant.pType !== 'Customer' &&
71
+ participant.pType !== 'Supervisor' &&
72
+ !participant.hasLeft &&
73
+ participant.pType !== 'VVA'
74
+ ) {
75
+ agentParticipants.add(participantId);
76
+ }
77
+ });
78
+ }
79
+
80
+ return agentParticipants.size >= 2;
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
+ };
@@ -17,9 +17,31 @@ export const PAUSE = '/record/pause';
17
17
  export const RESUME = '/record/resume';
18
18
  export const WRAPUP = '/wrapup';
19
19
  export const END = '/end';
20
+ export const CONSULT_CONFERENCE = '/consult/conference';
21
+ export const CONFERENCE_EXIT = '/conference/exit';
22
+ export const CONFERENCE_TRANSFER = '/conference/transfer';
20
23
  export const TASK_MANAGER_FILE = 'taskManager';
21
24
  export const TASK_FILE = 'task';
22
25
 
26
+ /**
27
+ * Task data field names that should be preserved during reconciliation
28
+ * These fields are retained even if not present in new data during updates
29
+ */
30
+ export const PRESERVED_TASK_DATA_FIELDS = {
31
+ /** Indicates if the task is in consultation state */
32
+ IS_CONSULTED: 'isConsulted',
33
+ /** Indicates if wrap-up is required for this task */
34
+ WRAP_UP_REQUIRED: 'wrapUpRequired',
35
+ /** Indicates if a conference is currently in progress (2+ active agents) */
36
+ IS_CONFERENCE_IN_PROGRESS: 'isConferenceInProgress',
37
+ };
38
+
39
+ /**
40
+ * Array of task data field names that should not be deleted during reconciliation
41
+ * Used by reconcileData method to preserve important task state fields
42
+ */
43
+ export const KEYS_TO_NOT_DELETE: string[] = Object.values(PRESERVED_TASK_DATA_FIELDS);
44
+
23
45
  // METHOD NAMES
24
46
  export const METHODS = {
25
47
  // Task class methods
@@ -36,6 +58,9 @@ export const METHODS = {
36
58
  END_CONSULT: 'endConsult',
37
59
  TRANSFER: 'transfer',
38
60
  CONSULT_TRANSFER: 'consultTransfer',
61
+ CONSULT_CONFERENCE: 'consultConference',
62
+ EXIT_CONFERENCE: 'exitConference',
63
+ TRANSFER_CONFERENCE: 'transferConference',
39
64
  UPDATE_TASK_DATA: 'updateTaskData',
40
65
  RECONCILE_DATA: 'reconcileData',
41
66