-
Notifications
You must be signed in to change notification settings - Fork 395
Expand file tree
/
Copy pathUtils.ts
More file actions
373 lines (324 loc) · 12.8 KB
/
Utils.ts
File metadata and controls
373 lines (324 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import * as Err from './Err';
import {LoginOption, WebexRequestPayload} from '../../types';
import {Failure, AugmentedError} from './GlobalTypes';
import LoggerProxy from '../../logger-proxy';
import WebexRequest from './WebexRequest';
import {
TaskData,
ConsultTransferPayLoad,
CONSULT_TRANSFER_DESTINATION_TYPE,
Interaction,
} from '../task/types';
import {PARTICIPANT_TYPES, STATE_CONSULT} from './constants';
import {DialPlan} from '../config/types';
/**
* Extracts common error details from a Webex request payload.
*
* @param errObj - The Webex request payload object.
* @returns An object containing the tracking ID and message body.
* @private
* @ignore
*/
const getCommonErrorDetails = (errObj: WebexRequestPayload) => {
return {
trackingId: errObj?.headers?.trackingid || errObj?.headers?.TrackingID,
msg: errObj?.body,
};
};
/**
* Checks if the destination type represents an entry point variant (EPDN or ENTRYPOINT).
*/
const isEntryPointOrEpdn = (destAgentType?: string): boolean => {
return destAgentType === 'EPDN' || destAgentType === 'ENTRYPOINT';
};
/**
* Determines if the task involves dialing a number based on the destination type.
* Returns 'DIAL_NUMBER' for dial-related destinations, empty string otherwise.
*/
const getAgentActionTypeFromTask = (taskData?: TaskData): 'DIAL_NUMBER' | '' => {
const destAgentType = taskData?.destinationType;
// Check if destination requires dialing: direct dial number or entry point variants
const isDialNumber = destAgentType === 'DN';
const isEntryPointVariant = isEntryPointOrEpdn(destAgentType);
// If the destination type is a dial number or an entry point variant, return 'DIAL_NUMBER'
return isDialNumber || isEntryPointVariant ? 'DIAL_NUMBER' : '';
};
// Fallback regex for US/Canada dial numbers when no dial plan entries are configured
export const FALLBACK_DIAL_NUMBER_REGEX = /1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}/;
/**
* Validates a dial number against the provided dial plan regex patterns.
* A number is valid if it matches at least one regex pattern in the dial plans.
* Falls back to US/Canada regex validation if no dial plan entries are configured.
*
* @param input - The dial number to validate
* @param dialPlanEntries - Array of dial plan entries containing regex patterns
* @returns true if the input matches at least one dial plan regex pattern, false otherwise
*/
export const isValidDialNumber = (
input: string,
dialPlanEntries: DialPlan['dialPlanEntity']
): boolean => {
if (!dialPlanEntries || dialPlanEntries.length === 0) {
LoggerProxy.info('No dial plan entries found. Falling back to US number validation.');
return FALLBACK_DIAL_NUMBER_REGEX.test(input);
}
return dialPlanEntries.some((entry) => {
try {
const regex = new RegExp(entry.regex);
return regex.test(input);
} catch {
return false;
}
});
};
export const getStationLoginErrorData = (failure: Failure, loginOption: LoginOption) => {
let duplicateLocationMessage = 'This value is already in use';
if (loginOption === LoginOption.EXTENSION) {
duplicateLocationMessage = 'This extension is already in use';
}
if (loginOption === LoginOption.AGENT_DN) {
duplicateLocationMessage =
'Dial number is in use. Try a different one. For help, reach out to your administrator or support team.';
}
const errorCodeMessageMap = {
DUPLICATE_LOCATION: {
message: duplicateLocationMessage,
fieldName: loginOption,
},
INVALID_DIAL_NUMBER: {
message:
'Enter a valid dial number. For help, reach out to your administrator or support team.',
fieldName: loginOption,
},
};
const defaultMessage = 'An error occurred while logging in to the station';
const defaultFieldName = 'generic';
const reason = failure?.data?.reason || '';
return {
message: errorCodeMessageMap[reason]?.message || defaultMessage,
fieldName: errorCodeMessageMap[reason]?.fieldName || defaultFieldName,
};
};
/**
* Extracts error details and logs the error. Also uploads logs for the error unless it is a silent relogin agent not found error.
*
* @param error - The error object, expected to have a `details` property of type Failure.
* @param methodName - The name of the method where the error occurred.
* @param moduleName - The name of the module where the error occurred.
* @returns An object containing the error instance and the reason string.
* @public
* @example
* const details = getErrorDetails(error, 'fetchData', 'DataModule');
* if (details.error) { handleError(details.error); }
* @ignore
*/
export const getErrorDetails = (error: any, methodName: string, moduleName: string) => {
let errData = {message: '', fieldName: ''};
const failure = error.details as Failure;
const reason = failure?.data?.reason ?? `Error while performing ${methodName}`;
if (!(reason === 'AGENT_NOT_FOUND' && methodName === 'silentRelogin')) {
LoggerProxy.error(`${methodName} failed with reason: ${reason}`, {
module: moduleName,
method: methodName,
trackingId: failure?.trackingId,
});
// we can add more conditions here if not needed for specific cases eg: silentReLogin
WebexRequest.getInstance().uploadLogs({
correlationId: failure?.trackingId,
});
}
if (methodName === 'stationLogin') {
errData = getStationLoginErrorData(failure, error.loginOption);
LoggerProxy.error(
`${methodName} failed with reason: ${reason}, message: ${errData.message}, fieldName: ${errData.fieldName}`,
{
module: moduleName,
method: methodName,
trackingId: failure?.trackingId,
}
);
}
const err = new Error(reason ?? `Error while performing ${methodName}`);
// @ts-ignore - add custom property to the error object for backward compatibility
err.data = errData;
return {
error: err,
reason,
};
};
/**
* Extracts error details from task API errors and logs them. Also uploads logs for the error.
* This handles the specific error format returned by task API calls.
*
* @param error - The error object from task API calls with structure: {id: string, details: {trackingId: string, msg: {...}}}
* @param methodName - The name of the method where the error occurred.
* @param moduleName - The name of the module where the error occurred.
* @returns AugmentedError containing structured error details on err.data for metrics and logging
* @public
* @example
* const taskError = generateTaskErrorObject(error, 'transfer', 'TaskModule');
* throw taskError.error;
* @ignore
*/
export const generateTaskErrorObject = (
error: any,
methodName: string,
moduleName: string
): AugmentedError => {
const trackingId = error?.details?.trackingId || error?.trackingId || '';
const errorMsg = error?.details?.msg;
const fallbackMessage =
(error && typeof error.message === 'string' && error.message) ||
`Error while performing ${methodName}`;
const errorMessage = errorMsg?.errorMessage || fallbackMessage;
const errorType =
errorMsg?.errorType ||
(error && typeof error.name === 'string' && error.name) ||
'Unknown Error';
const errorData = errorMsg?.errorData || '';
const reasonCode = errorMsg?.reasonCode || 0;
// Log and upload for Task API formatted errors
LoggerProxy.error(`${methodName} failed: ${errorMessage} (${errorType})`, {
module: moduleName,
method: methodName,
trackingId,
});
WebexRequest.getInstance().uploadLogs({
correlationId: trackingId,
});
const reason = `${errorType}: ${errorMessage}${errorData ? ` (${errorData})` : ''}`;
const err: AugmentedError = new Error(reason);
err.data = {
message: errorMessage,
errorType,
errorData,
reasonCode,
trackingId,
};
return err;
};
/**
* Creates an error details object suitable for use with the Err.Details class.
*
* @param errObj - The Webex request payload object.
* @returns An instance of Err.Details with the generic failure message and extracted details.
* @public
* @example
* const errDetails = createErrDetailsObject(webexRequestPayload);
* @ignore
*/
export const createErrDetailsObject = (errObj: WebexRequestPayload) => {
const details = getCommonErrorDetails(errObj);
return new Err.Details('Service.reqs.generic.failure', details);
};
/**
* Gets the consulted agent ID from the media object by finding the agent
* in the consult media participants (excluding the current agent).
*
* @param media - The media object from the interaction
* @param agentId - The current agent's ID to exclude from the search
* @returns The consulted agent ID, or empty string if none found
*/
export const getConsultedAgentId = (media: Interaction['media'], agentId: string): string => {
let consultParticipants: string[] = [];
let consultedParticipantId = '';
Object.keys(media).forEach((key) => {
if (media[key].mType === STATE_CONSULT) {
consultParticipants = media[key].participants;
}
});
if (consultParticipants.includes(agentId)) {
const id = consultParticipants.find((participant) => participant !== agentId);
consultedParticipantId = id || consultedParticipantId;
}
return consultedParticipantId;
};
/**
* Gets the destination agent ID for CBT (Capacity Based Team) scenarios.
* CBT refers to teams created in Control Hub with capacity-based routing
* (as opposed to agent-based routing). This handles cases where the consulted
* participant is not directly in participants but can be found by matching
* the dial number (dn).
*
* @param interaction - The interaction object
* @param consultingAgent - The consulting agent identifier
* @returns The destination agent ID for CBT scenarios, or empty string if none found
*/
export const getDestAgentIdForCBT = (interaction: Interaction, consultingAgent: string): string => {
const participants = interaction.participants;
let destAgentIdForCBT = '';
// Check if this is a CBT scenario (consultingAgent exists but not directly in participants)
if (consultingAgent && !participants[consultingAgent]) {
const foundEntry = Object.entries(participants).find(
([, participant]: [string, Interaction['participants'][string]]) => {
return (
participant.pType.toLowerCase() === PARTICIPANT_TYPES.DN &&
participant.type === PARTICIPANT_TYPES.AGENT &&
participant.dn === consultingAgent
);
}
);
if (foundEntry) {
destAgentIdForCBT = foundEntry[0];
}
}
return destAgentIdForCBT;
};
/**
* Calculates the destination agent ID for consult operations.
*
* @param interaction - The interaction object
* @param agentId - The current agent's ID
* @returns The destination agent ID
*/
export const calculateDestAgentId = (interaction: Interaction, agentId: string): string => {
const consultingAgent = getConsultedAgentId(interaction.media, agentId);
// Check if this is a CBT (Capacity Based Team) scenario
// If not CBT, the function will return empty string and we'll use the normal flow
const destAgentIdCBT = getDestAgentIdForCBT(interaction, consultingAgent);
if (destAgentIdCBT) {
return destAgentIdCBT;
}
return interaction.participants[consultingAgent]?.type === PARTICIPANT_TYPES.EP_DN
? interaction.participants[consultingAgent]?.epId
: interaction.participants[consultingAgent]?.id;
};
/**
* Calculates the destination agent ID for fetching destination type.
*
* @param interaction - The interaction object
* @param agentId - The current agent's ID
* @returns The destination agent ID for determining destination type
*/
export const calculateDestType = (interaction: Interaction, agentId: string): string => {
const consultingAgent = getConsultedAgentId(interaction.media, agentId);
// Check if this is a CBT (Capacity Based Team) scenario, otherwise use consultingAgent
const destAgentIdCBT = getDestAgentIdForCBT(interaction, consultingAgent);
const destinationaegntId = destAgentIdCBT || consultingAgent;
const destAgentType = destinationaegntId
? interaction.participants[destinationaegntId]?.pType
: undefined;
if (destAgentType) {
if (destAgentType === 'DN') {
return CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
}
if (destAgentType === 'EP-DN') {
return CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT;
}
// Keep the existing destinationType if it's something else (like "agent" or "Agent")
// Convert "Agent" to lowercase for consistency
return destAgentType.toLowerCase();
}
return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
};
export const deriveConsultTransferDestinationType = (
taskData?: TaskData
): ConsultTransferPayLoad['destinationType'] => {
const agentActionType = getAgentActionTypeFromTask(taskData);
if (agentActionType === 'DIAL_NUMBER') {
return isEntryPointOrEpdn(taskData?.destinationType)
? CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT
: CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
}
return CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
};