@webex/contact-center 3.9.0 → 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.
- package/dist/cc.js +207 -47
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/logger-proxy.js +24 -1
- package/dist/logger-proxy.js.map +1 -1
- package/dist/metrics/MetricsManager.js +1 -1
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +89 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +32 -2
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AddressBook.js +271 -0
- package/dist/services/AddressBook.js.map +1 -0
- package/dist/services/EntryPoint.js +227 -0
- package/dist/services/EntryPoint.js.map +1 -0
- package/dist/services/Queue.js +261 -0
- package/dist/services/Queue.js.map +1 -0
- package/dist/services/config/constants.js +36 -2
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +29 -21
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +33 -1
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/GlobalTypes.js.map +1 -1
- package/dist/services/core/Utils.js +181 -2
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +0 -4
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/constants.js +17 -1
- package/dist/services/core/constants.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +0 -4
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/task/TaskManager.js +151 -7
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +104 -0
- package/dist/services/task/TaskUtils.js.map +1 -0
- package/dist/services/task/constants.js +26 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/contact.js +86 -0
- package/dist/services/task/contact.js.map +1 -1
- package/dist/services/task/index.js +428 -91
- package/dist/services/task/index.js.map +1 -1
- package/dist/services/task/types.js +12 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/types/cc.d.ts +121 -35
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/metrics/constants.d.ts +25 -1
- package/dist/types/services/AddressBook.d.ts +74 -0
- package/dist/types/services/EntryPoint.d.ts +67 -0
- package/dist/types/services/Queue.d.ts +76 -0
- package/dist/types/services/config/constants.d.ts +35 -1
- package/dist/types/services/config/index.d.ts +6 -9
- package/dist/types/services/config/types.d.ts +79 -58
- package/dist/types/services/core/GlobalTypes.d.ts +25 -0
- package/dist/types/services/core/Utils.d.ts +55 -1
- package/dist/types/services/core/constants.d.ts +14 -0
- package/dist/types/services/task/TaskUtils.d.ts +42 -0
- package/dist/types/services/task/constants.d.ts +23 -0
- package/dist/types/services/task/contact.d.ts +10 -0
- package/dist/types/services/task/index.d.ts +85 -4
- package/dist/types/services/task/types.d.ts +245 -21
- package/dist/types/types.d.ts +162 -0
- package/dist/types/utils/PageCache.d.ts +173 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/PageCache.js +192 -0
- package/dist/utils/PageCache.js.map +1 -0
- package/dist/webex.js +1 -1
- package/package.json +10 -9
- package/src/cc.ts +232 -52
- package/src/constants.ts +1 -0
- package/src/index.ts +17 -2
- package/src/logger-proxy.ts +24 -1
- package/src/metrics/MetricsManager.ts +1 -1
- package/src/metrics/behavioral-events.ts +94 -0
- package/src/metrics/constants.ts +37 -1
- package/src/services/AddressBook.ts +291 -0
- package/src/services/EntryPoint.ts +241 -0
- package/src/services/Queue.ts +277 -0
- package/src/services/config/constants.ts +42 -2
- package/src/services/config/index.ts +30 -30
- package/src/services/config/types.ts +59 -58
- package/src/services/core/GlobalTypes.ts +27 -0
- package/src/services/core/Utils.ts +215 -1
- package/src/services/core/aqm-reqs.ts +0 -5
- package/src/services/core/constants.ts +16 -0
- package/src/services/core/websocket/WebSocketManager.ts +0 -4
- package/src/services/task/TaskManager.ts +182 -9
- package/src/services/task/TaskUtils.ts +113 -0
- package/src/services/task/constants.ts +25 -0
- package/src/services/task/contact.ts +80 -0
- package/src/services/task/index.ts +497 -71
- package/src/services/task/types.ts +264 -20
- package/src/types.ts +180 -0
- package/src/utils/PageCache.ts +252 -0
- package/test/unit/spec/cc.ts +282 -85
- package/test/unit/spec/metrics/MetricsManager.ts +0 -1
- package/test/unit/spec/metrics/behavioral-events.ts +42 -0
- package/test/unit/spec/services/AddressBook.ts +332 -0
- package/test/unit/spec/services/EntryPoint.ts +259 -0
- package/test/unit/spec/services/Queue.ts +323 -0
- package/test/unit/spec/services/config/index.ts +279 -65
- package/test/unit/spec/services/core/Utils.ts +282 -1
- package/test/unit/spec/services/core/aqm-reqs.ts +1 -3
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +0 -4
- package/test/unit/spec/services/task/TaskManager.ts +760 -2
- package/test/unit/spec/services/task/TaskUtils.ts +131 -0
- package/test/unit/spec/services/task/contact.ts +31 -1
- package/test/unit/spec/services/task/index.ts +873 -163
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import * as Err from './Err';
|
|
2
2
|
import {LoginOption, WebexRequestPayload} from '../../types';
|
|
3
|
-
import {Failure} from './GlobalTypes';
|
|
3
|
+
import {Failure, AugmentedError} from './GlobalTypes';
|
|
4
4
|
import LoggerProxy from '../../logger-proxy';
|
|
5
5
|
import WebexRequest from './WebexRequest';
|
|
6
|
+
import {
|
|
7
|
+
TaskData,
|
|
8
|
+
ConsultTransferPayLoad,
|
|
9
|
+
CONSULT_TRANSFER_DESTINATION_TYPE,
|
|
10
|
+
Interaction,
|
|
11
|
+
} from '../task/types';
|
|
12
|
+
import {PARTICIPANT_TYPES, STATE_CONSULT} from './constants';
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Extracts common error details from a Webex request payload.
|
|
@@ -19,6 +26,28 @@ const getCommonErrorDetails = (errObj: WebexRequestPayload) => {
|
|
|
19
26
|
};
|
|
20
27
|
};
|
|
21
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Checks if the destination type represents an entry point variant (EPDN or ENTRYPOINT).
|
|
31
|
+
*/
|
|
32
|
+
const isEntryPointOrEpdn = (destAgentType?: string): boolean => {
|
|
33
|
+
return destAgentType === 'EPDN' || destAgentType === 'ENTRYPOINT';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determines if the task involves dialing a number based on the destination type.
|
|
38
|
+
* Returns 'DIAL_NUMBER' for dial-related destinations, empty string otherwise.
|
|
39
|
+
*/
|
|
40
|
+
const getAgentActionTypeFromTask = (taskData?: TaskData): 'DIAL_NUMBER' | '' => {
|
|
41
|
+
const destAgentType = taskData?.destinationType;
|
|
42
|
+
|
|
43
|
+
// Check if destination requires dialing: direct dial number or entry point variants
|
|
44
|
+
const isDialNumber = destAgentType === 'DN';
|
|
45
|
+
const isEntryPointVariant = isEntryPointOrEpdn(destAgentType);
|
|
46
|
+
|
|
47
|
+
// If the destination type is a dial number or an entry point variant, return 'DIAL_NUMBER'
|
|
48
|
+
return isDialNumber || isEntryPointVariant ? 'DIAL_NUMBER' : '';
|
|
49
|
+
};
|
|
50
|
+
|
|
22
51
|
export const isValidDialNumber = (input: string): boolean => {
|
|
23
52
|
// This regex checks for a valid dial number format for only few countries such as US, Canada.
|
|
24
53
|
const regexForDn = /1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}/;
|
|
@@ -115,6 +144,62 @@ export const getErrorDetails = (error: any, methodName: string, moduleName: stri
|
|
|
115
144
|
};
|
|
116
145
|
};
|
|
117
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Extracts error details from task API errors and logs them. Also uploads logs for the error.
|
|
149
|
+
* This handles the specific error format returned by task API calls.
|
|
150
|
+
*
|
|
151
|
+
* @param error - The error object from task API calls with structure: {id: string, details: {trackingId: string, msg: {...}}}
|
|
152
|
+
* @param methodName - The name of the method where the error occurred.
|
|
153
|
+
* @param moduleName - The name of the module where the error occurred.
|
|
154
|
+
* @returns AugmentedError containing structured error details on err.data for metrics and logging
|
|
155
|
+
* @public
|
|
156
|
+
* @example
|
|
157
|
+
* const taskError = generateTaskErrorObject(error, 'transfer', 'TaskModule');
|
|
158
|
+
* throw taskError.error;
|
|
159
|
+
* @ignore
|
|
160
|
+
*/
|
|
161
|
+
export const generateTaskErrorObject = (
|
|
162
|
+
error: any,
|
|
163
|
+
methodName: string,
|
|
164
|
+
moduleName: string
|
|
165
|
+
): AugmentedError => {
|
|
166
|
+
const trackingId = error?.details?.trackingId || error?.trackingId || '';
|
|
167
|
+
const errorMsg = error?.details?.msg;
|
|
168
|
+
|
|
169
|
+
const fallbackMessage =
|
|
170
|
+
(error && typeof error.message === 'string' && error.message) ||
|
|
171
|
+
`Error while performing ${methodName}`;
|
|
172
|
+
const errorMessage = errorMsg?.errorMessage || fallbackMessage;
|
|
173
|
+
const errorType =
|
|
174
|
+
errorMsg?.errorType ||
|
|
175
|
+
(error && typeof error.name === 'string' && error.name) ||
|
|
176
|
+
'Unknown Error';
|
|
177
|
+
const errorData = errorMsg?.errorData || '';
|
|
178
|
+
const reasonCode = errorMsg?.reasonCode || 0;
|
|
179
|
+
|
|
180
|
+
// Log and upload for Task API formatted errors
|
|
181
|
+
LoggerProxy.error(`${methodName} failed: ${errorMessage} (${errorType})`, {
|
|
182
|
+
module: moduleName,
|
|
183
|
+
method: methodName,
|
|
184
|
+
trackingId,
|
|
185
|
+
});
|
|
186
|
+
WebexRequest.getInstance().uploadLogs({
|
|
187
|
+
correlationId: trackingId,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const reason = `${errorType}: ${errorMessage}${errorData ? ` (${errorData})` : ''}`;
|
|
191
|
+
const err: AugmentedError = new Error(reason);
|
|
192
|
+
err.data = {
|
|
193
|
+
message: errorMessage,
|
|
194
|
+
errorType,
|
|
195
|
+
errorData,
|
|
196
|
+
reasonCode,
|
|
197
|
+
trackingId,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return err;
|
|
201
|
+
};
|
|
202
|
+
|
|
118
203
|
/**
|
|
119
204
|
* Creates an error details object suitable for use with the Err.Details class.
|
|
120
205
|
*
|
|
@@ -130,3 +215,132 @@ export const createErrDetailsObject = (errObj: WebexRequestPayload) => {
|
|
|
130
215
|
|
|
131
216
|
return new Err.Details('Service.reqs.generic.failure', details);
|
|
132
217
|
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
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
|
+
*
|
|
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).
|
|
251
|
+
*
|
|
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
|
|
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
|
+
|
|
280
|
+
/**
|
|
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
|
|
286
|
+
*/
|
|
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;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Calculates the destination agent ID for fetching destination type.
|
|
304
|
+
*
|
|
305
|
+
* @param interaction - The interaction object
|
|
306
|
+
* @param agentId - The current agent's ID
|
|
307
|
+
* @returns The destination agent ID for determining destination type
|
|
308
|
+
*/
|
|
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();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const deriveConsultTransferDestinationType = (
|
|
335
|
+
taskData?: TaskData
|
|
336
|
+
): ConsultTransferPayLoad['destinationType'] => {
|
|
337
|
+
const agentActionType = getAgentActionTypeFromTask(taskData);
|
|
338
|
+
|
|
339
|
+
if (agentActionType === 'DIAL_NUMBER') {
|
|
340
|
+
return isEntryPointOrEpdn(taskData?.destinationType)
|
|
341
|
+
? CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT
|
|
342
|
+
: CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
|
|
346
|
+
};
|
|
@@ -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
|
|
@@ -178,10 +178,6 @@ export class WebSocketManager extends EventEmitter {
|
|
|
178
178
|
issueReason = 'WebSocket auto close timed out. Forcefully closed websocket.';
|
|
179
179
|
} else {
|
|
180
180
|
const onlineStatus = navigator.onLine;
|
|
181
|
-
LoggerProxy.info(`[WebSocketStatus] | desktop online status is ${onlineStatus}`, {
|
|
182
|
-
module: WEB_SOCKET_MANAGER_FILE,
|
|
183
|
-
method: METHODS.WEB_SOCKET_ON_CLOSE_HANDLER,
|
|
184
|
-
});
|
|
185
181
|
issueReason = !onlineStatus
|
|
186
182
|
? 'network issue'
|
|
187
183
|
: 'missing keepalive from either desktop or notif service';
|
|
@@ -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?.[
|
|
118
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -270,6 +304,7 @@ export default class TaskManager extends EventEmitter {
|
|
|
270
304
|
break;
|
|
271
305
|
case CC_EVENTS.AGENT_CONSULTING:
|
|
272
306
|
// Received when agent is in an active consult state
|
|
307
|
+
// TODO: Check if we can use backend consult state instead of isConsulted
|
|
273
308
|
task = this.updateTaskData(task, payload.data);
|
|
274
309
|
if (task.data.isConsulted) {
|
|
275
310
|
// Fire only if you are the agent who received the consult request
|
|
@@ -323,6 +358,89 @@ export default class TaskManager extends EventEmitter {
|
|
|
323
358
|
task = this.updateTaskData(task, payload.data);
|
|
324
359
|
task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
|
|
325
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;
|
|
326
444
|
default:
|
|
327
445
|
break;
|
|
328
446
|
}
|
|
@@ -361,6 +479,54 @@ export default class TaskManager extends EventEmitter {
|
|
|
361
479
|
}
|
|
362
480
|
}
|
|
363
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
|
+
|
|
364
530
|
private removeTaskFromCollection(task: ITask) {
|
|
365
531
|
if (task?.data?.interactionId) {
|
|
366
532
|
delete this.taskCollection[task.data.interactionId];
|
|
@@ -372,7 +538,13 @@ export default class TaskManager extends EventEmitter {
|
|
|
372
538
|
}
|
|
373
539
|
}
|
|
374
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
|
+
*/
|
|
375
546
|
private handleTaskCleanup(task: ITask) {
|
|
547
|
+
// Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
|
|
376
548
|
if (
|
|
377
549
|
this.webCallingService.loginOption === LoginOption.BROWSER &&
|
|
378
550
|
task.data.interaction.mediaType === 'telephony'
|
|
@@ -380,8 +552,9 @@ export default class TaskManager extends EventEmitter {
|
|
|
380
552
|
task.unregisterWebCallListeners();
|
|
381
553
|
this.webCallingService.cleanUpCall();
|
|
382
554
|
}
|
|
383
|
-
|
|
384
|
-
|
|
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,
|
|
385
558
|
// retain tasks until they complete wrap-up, unless the task disconnected before being answered.
|
|
386
559
|
this.removeTaskFromCollection(task);
|
|
387
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
|
+
};
|