@webex/contact-center 3.12.0-task-refactor.4 → 3.12.0-task-refactor.6
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/services/task/TaskManager.js +1 -0
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +8 -6
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +77 -14
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +85 -13
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +35 -0
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +76 -10
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/voice/Voice.js +10 -4
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +68 -8
- package/dist/types/services/task/state-machine/guards.d.ts +5 -0
- package/dist/types/services/task/voice/Voice.d.ts +18 -17
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/TaskManager.ts +1 -1
- package/src/services/task/TaskUtils.ts +8 -6
- package/src/services/task/state-machine/TaskStateMachine.ts +101 -16
- package/src/services/task/state-machine/actions.ts +148 -24
- package/src/services/task/state-machine/guards.ts +46 -0
- package/src/services/task/state-machine/uiControlsComputer.ts +158 -15
- package/src/services/task/voice/Voice.ts +12 -5
- package/test/unit/spec/services/WebCallingService.ts +7 -1
- package/test/unit/spec/services/task/TaskManager.ts +26 -0
- package/test/unit/spec/services/task/TaskUtils.ts +16 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +573 -0
- package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +1023 -46
- package/test/unit/spec/services/task/voice/Voice.ts +44 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -144,16 +144,27 @@ function computeVoiceInteractionUIControls(
|
|
|
144
144
|
Boolean(selfAgentId) &&
|
|
145
145
|
Boolean(mainCallId) &&
|
|
146
146
|
Boolean(interaction?.media?.[mainCallId]?.participants?.includes(selfAgentId as string));
|
|
147
|
+
const consultMedia = Object.values(interaction?.media ?? {}).find(
|
|
148
|
+
(media: any) =>
|
|
149
|
+
media?.mediaResourceId === taskData?.consultMediaResourceId || media?.mType === 'consult'
|
|
150
|
+
) as {participants?: string[]} | undefined;
|
|
151
|
+
const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
|
|
152
|
+
const selfInConsultCall =
|
|
153
|
+
Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
|
|
147
154
|
const conferenceActive = isConferencing || conferenceFromBackend || consultFromConference;
|
|
148
155
|
// Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
|
|
149
156
|
const inConference = conferenceActive && (isConferencing || selfInMainCall || consultInitiator);
|
|
150
157
|
|
|
151
158
|
// Check if this is a consulted agent (must be after isConsulting is computed).
|
|
152
|
-
const isSoleAgentOnCall =
|
|
159
|
+
const isSoleAgentOnCall =
|
|
160
|
+
participantCount <= 1 && selfInMainCall && !isConsulting && !inConference;
|
|
153
161
|
const isConsulted =
|
|
154
162
|
inConference || isSoleAgentOnCall
|
|
155
163
|
? false
|
|
156
|
-
: getIsConsultedAgentForControls(taskData, context, isConsulting)
|
|
164
|
+
: getIsConsultedAgentForControls(taskData, context, isConsulting) ||
|
|
165
|
+
(!consultInitiator &&
|
|
166
|
+
(selfParticipant?.isConsulted === true ||
|
|
167
|
+
selfParticipant?.consultState === 'consulting'));
|
|
157
168
|
|
|
158
169
|
// Active call = can perform call operations
|
|
159
170
|
const isActive =
|
|
@@ -173,29 +184,111 @@ function computeVoiceInteractionUIControls(
|
|
|
173
184
|
taskData?.consultMediaResourceId ||
|
|
174
185
|
Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
|
|
175
186
|
);
|
|
187
|
+
const selfConsultPendingOnConsultMedia =
|
|
188
|
+
selfParticipant?.consultState === 'consultInitiated' &&
|
|
189
|
+
!taskData?.isConsulted &&
|
|
190
|
+
hasConsultMedia;
|
|
191
|
+
const ownerParticipant = interaction?.owner
|
|
192
|
+
? interaction.participants?.[interaction.owner]
|
|
193
|
+
: undefined;
|
|
194
|
+
const otherAgentConsultInProgress = Boolean(
|
|
195
|
+
interaction?.participants &&
|
|
196
|
+
Object.values(interaction.participants).some((participant: any) => {
|
|
197
|
+
if (!participant || participant.hasLeft) return false;
|
|
198
|
+
if (participant.id === selfAgentId) return false;
|
|
199
|
+
if (participant.pType !== 'AGENT') return false;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
participant.consultState === 'consultInitiated' ||
|
|
203
|
+
participant.consultState === 'consultReserved' ||
|
|
204
|
+
participant.consultState === 'consulting' ||
|
|
205
|
+
participant.currentState === 'consulting'
|
|
206
|
+
);
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
const isHydratedConferenceConsultPending =
|
|
210
|
+
inConference && selfConsultPendingOnConsultMedia && !consultDestinationAgentJoined;
|
|
176
211
|
const hasParallelConsultLeg =
|
|
177
212
|
consultOwnedBySelf &&
|
|
178
213
|
!isConsulting &&
|
|
179
214
|
!isConsulted &&
|
|
180
215
|
(consultInProgress || consultCallHeld || hasConsultMedia);
|
|
216
|
+
const activeLegForConferenceConsult = consultCallHeld ? 'main' : 'consult';
|
|
217
|
+
const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
|
|
218
|
+
const isConferenceConsultTransferContext =
|
|
219
|
+
inConference && consultInitiator && hasConsultMedia && isConsultDestinationReady;
|
|
181
220
|
const consultLegOnHold = isConsulting && consultCallHeld;
|
|
221
|
+
const callProcessingDetails = interaction?.callProcessingDetails as
|
|
222
|
+
| {conferenceHoldParticipant?: boolean | string}
|
|
223
|
+
| undefined;
|
|
224
|
+
const conferenceHoldParticipant =
|
|
225
|
+
callProcessingDetails?.conferenceHoldParticipant === true ||
|
|
226
|
+
callProcessingDetails?.conferenceHoldParticipant === 'true';
|
|
227
|
+
const postDeclineHeldMainLeg =
|
|
228
|
+
consultInitiator &&
|
|
229
|
+
!consultDestinationAgentJoined &&
|
|
230
|
+
isHeld &&
|
|
231
|
+
inConference &&
|
|
232
|
+
conferenceHoldParticipant;
|
|
233
|
+
const postConsultCompletedHeldMainLeg =
|
|
234
|
+
selfParticipant?.consultState === 'consultCompleted' && isHeld && inConference && !isConsulting;
|
|
235
|
+
const nonOwnerPostConsultCompletedHeldMainLeg =
|
|
236
|
+
isHeld &&
|
|
237
|
+
inConference &&
|
|
238
|
+
!isConsulting &&
|
|
239
|
+
!consultInitiator &&
|
|
240
|
+
Boolean(selfAgentId) &&
|
|
241
|
+
Boolean(interaction?.owner) &&
|
|
242
|
+
selfAgentId !== interaction?.owner &&
|
|
243
|
+
ownerParticipant?.consultState === 'consultCompleted';
|
|
244
|
+
const isConsultPendingBeforeJoin =
|
|
245
|
+
selfParticipant?.consultState === 'consultInitiated' && !consultDestinationAgentJoined;
|
|
246
|
+
const hideExitConferenceWhileConsultPending =
|
|
247
|
+
currentLeg === 'main' &&
|
|
248
|
+
inConference &&
|
|
249
|
+
isConsultPendingBeforeJoin &&
|
|
250
|
+
(consultFromConference ||
|
|
251
|
+
consultInitiator ||
|
|
252
|
+
taskData?.type === 'AgentConsultCreated' ||
|
|
253
|
+
consultInProgress ||
|
|
254
|
+
isConsulting);
|
|
255
|
+
const hideExitConferenceDuringActiveConsultFromConference =
|
|
256
|
+
inConference &&
|
|
257
|
+
consultInitiator &&
|
|
258
|
+
consultDestinationAgentJoined &&
|
|
259
|
+
(isConsulting ||
|
|
260
|
+
taskData?.type === 'AgentConsulting' ||
|
|
261
|
+
selfParticipant?.consultState === 'consulting');
|
|
262
|
+
const forceHeldPostConsultControls =
|
|
263
|
+
!hideExitConferenceWhileConsultPending &&
|
|
264
|
+
(postDeclineHeldMainLeg || postConsultCompletedHeldMainLeg);
|
|
265
|
+
const selfOnConsultLeg =
|
|
266
|
+
selfParticipant?.consultState === 'consulting' ||
|
|
267
|
+
selfParticipant?.currentState === 'consulting';
|
|
268
|
+
const showMainLegConferenceControlsDuringConsult =
|
|
269
|
+
currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
|
|
270
|
+
const allowHeldMainLegControlsForNonInitiator =
|
|
271
|
+
showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
|
|
182
272
|
|
|
183
273
|
return {
|
|
184
274
|
// Accept/Decline: Voice tasks in offered state
|
|
185
|
-
//
|
|
186
|
-
//
|
|
275
|
+
// Desktop/WebRTC + inbound: accept enabled (agent manually accepts)
|
|
276
|
+
// Desktop/WebRTC + outdial: accept disabled (auto-answer handles it; Widgets show "Accept" disabled)
|
|
277
|
+
// Extension mode (non-WebRTC): accept disabled (Widgets show "Ringing...")
|
|
187
278
|
accept:
|
|
188
279
|
state === TaskState.OFFERED && !interaction?.isTerminated
|
|
189
280
|
? {isVisible: true, isEnabled: isWebrtc && !isOutdial}
|
|
190
281
|
: DISABLED,
|
|
191
|
-
decline:
|
|
192
|
-
isWebrtc
|
|
193
|
-
|
|
194
|
-
|
|
282
|
+
decline: (() => {
|
|
283
|
+
if (!isWebrtc || state !== TaskState.OFFERED || interaction?.isTerminated) return DISABLED;
|
|
284
|
+
|
|
285
|
+
return isOutdial ? VISIBLE_DISABLED : VISIBLE_ENABLED;
|
|
286
|
+
})(),
|
|
195
287
|
|
|
196
288
|
// Hold: visible in connected/held/conference, disabled in conference/consulting
|
|
197
289
|
hold: (() => {
|
|
198
290
|
if (!hasFullControls) return DISABLED;
|
|
291
|
+
if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
|
|
199
292
|
if (consultOwnedBySelf && (isConsulting || hasParallelConsultLeg || consultCallHeld)) {
|
|
200
293
|
return DISABLED;
|
|
201
294
|
}
|
|
@@ -214,6 +307,7 @@ function computeVoiceInteractionUIControls(
|
|
|
214
307
|
mute: (() => {
|
|
215
308
|
if (!isWebrtc) return DISABLED;
|
|
216
309
|
if (isWrappingUp) return DISABLED;
|
|
310
|
+
if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
|
|
217
311
|
if (isConsulting) return VISIBLE_ENABLED;
|
|
218
312
|
|
|
219
313
|
if (isConnected || isHeld || isConferencing) {
|
|
@@ -227,6 +321,9 @@ function computeVoiceInteractionUIControls(
|
|
|
227
321
|
|
|
228
322
|
// End: varies by state; during consulting only on main leg (consult held)
|
|
229
323
|
end: (() => {
|
|
324
|
+
if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
|
|
325
|
+
if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
|
|
326
|
+
if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
|
|
230
327
|
if (!config.isEndTaskEnabled) return DISABLED;
|
|
231
328
|
if (hasParallelConsultLeg) {
|
|
232
329
|
return isConnected && isEpDnConsult ? VISIBLE_ENABLED : VISIBLE_DISABLED;
|
|
@@ -240,6 +337,7 @@ function computeVoiceInteractionUIControls(
|
|
|
240
337
|
|
|
241
338
|
if (inConference) {
|
|
242
339
|
if (isConsulted) return DISABLED;
|
|
340
|
+
if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
|
|
243
341
|
|
|
244
342
|
if (consultInProgress) return VISIBLE_DISABLED;
|
|
245
343
|
|
|
@@ -253,6 +351,11 @@ function computeVoiceInteractionUIControls(
|
|
|
253
351
|
|
|
254
352
|
// Transfer: connected/held/conference
|
|
255
353
|
transfer: (() => {
|
|
354
|
+
if (isHydratedConferenceConsultPending) return VISIBLE_DISABLED;
|
|
355
|
+
if (isConferenceConsultTransferContext && currentLeg === 'main' && isCurrentLegActive) {
|
|
356
|
+
return DISABLED;
|
|
357
|
+
}
|
|
358
|
+
if (inConference && isConsulting && consultInitiator) return DISABLED;
|
|
256
359
|
if (hasParallelConsultLeg) {
|
|
257
360
|
if (!customerPresent) return DISABLED;
|
|
258
361
|
if (state === TaskState.CONNECTED) return VISIBLE_ENABLED;
|
|
@@ -280,6 +383,7 @@ function computeVoiceInteractionUIControls(
|
|
|
280
383
|
consult: (() => {
|
|
281
384
|
const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
|
|
282
385
|
|
|
386
|
+
if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
|
|
283
387
|
if (hasParallelConsultLeg) return DISABLED;
|
|
284
388
|
if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
|
|
285
389
|
return DISABLED;
|
|
@@ -291,7 +395,11 @@ function computeVoiceInteractionUIControls(
|
|
|
291
395
|
if (participantCount <= 1) return VISIBLE_DISABLED;
|
|
292
396
|
// Real conference: consult enabled if conditions met
|
|
293
397
|
const canFromConference =
|
|
294
|
-
!maxParticipants &&
|
|
398
|
+
!maxParticipants &&
|
|
399
|
+
customerPresent &&
|
|
400
|
+
!consultInProgress &&
|
|
401
|
+
!otherAgentConsultInProgress &&
|
|
402
|
+
!isConsulting;
|
|
295
403
|
|
|
296
404
|
return {isVisible: true, isEnabled: canFromConference};
|
|
297
405
|
}
|
|
@@ -337,6 +445,7 @@ function computeVoiceInteractionUIControls(
|
|
|
337
445
|
// Conference: during consulting, enabled on both legs when agent joined
|
|
338
446
|
// Label changes based on leg: "Conference" on main leg, "Merge" on consult leg
|
|
339
447
|
conference: (() => {
|
|
448
|
+
if (isHydratedConferenceConsultPending && currentLeg === 'main') return VISIBLE_DISABLED;
|
|
340
449
|
if (hasParallelConsultLeg) {
|
|
341
450
|
if (!customerPresent) return DISABLED;
|
|
342
451
|
if (state === TaskState.CONNECTED) {
|
|
@@ -359,9 +468,15 @@ function computeVoiceInteractionUIControls(
|
|
|
359
468
|
|
|
360
469
|
// ExitConference: in conference with multiple agents in main call
|
|
361
470
|
exitConference: (() => {
|
|
471
|
+
if (hideExitConferenceWhileConsultPending) return DISABLED;
|
|
472
|
+
if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
|
|
473
|
+
if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
|
|
474
|
+
if (hideExitConferenceDuringActiveConsultFromConference) return DISABLED;
|
|
475
|
+
if (forceHeldPostConsultControls) return VISIBLE_DISABLED;
|
|
362
476
|
if (isConsulted && !isConferencing) return DISABLED;
|
|
363
477
|
if (!inConference) return DISABLED;
|
|
364
478
|
if (participantCount <= 1) return DISABLED;
|
|
479
|
+
if (consultInProgress) return VISIBLE_DISABLED;
|
|
365
480
|
const consultingFromConference = consultInitiator && isConsulting && conferenceFromBackend;
|
|
366
481
|
|
|
367
482
|
return consultingFromConference ? VISIBLE_DISABLED : VISIBLE_ENABLED;
|
|
@@ -369,8 +484,19 @@ function computeVoiceInteractionUIControls(
|
|
|
369
484
|
|
|
370
485
|
// TransferConference: in conference with active consult, owner consulting from conference
|
|
371
486
|
transferConference: (() => {
|
|
372
|
-
if (
|
|
373
|
-
|
|
487
|
+
if (isConferenceConsultTransferContext && !isCurrentLegActive) return VISIBLE_DISABLED;
|
|
488
|
+
const consultLegTransferAvailable =
|
|
489
|
+
currentLeg === 'consult' && inConference && consultInitiator && hasConsultMedia;
|
|
490
|
+
const selfConsultingOnParticipantState = selfParticipant?.consultState === 'consulting';
|
|
491
|
+
const conferenceTransferAvailable =
|
|
492
|
+
consultLegTransferAvailable ||
|
|
493
|
+
(inConference &&
|
|
494
|
+
consultInitiator &&
|
|
495
|
+
hasConsultMedia &&
|
|
496
|
+
(isConsulting || consultInProgress || selfConsultingOnParticipantState));
|
|
497
|
+
if (consultLegOnHold) return DISABLED;
|
|
498
|
+
if (hasParallelConsultLeg && !conferenceTransferAvailable) return DISABLED;
|
|
499
|
+
if (!conferenceTransferAvailable && (!inConference || !isConsulting)) return DISABLED;
|
|
374
500
|
if (!consultInitiator || isConsulted) return DISABLED;
|
|
375
501
|
|
|
376
502
|
return isConsultDestinationReady ? VISIBLE_ENABLED : VISIBLE_DISABLED;
|
|
@@ -378,6 +504,7 @@ function computeVoiceInteractionUIControls(
|
|
|
378
504
|
|
|
379
505
|
// MergeToConference: mirrors conference control, enabled on both legs
|
|
380
506
|
mergeToConference: (() => {
|
|
507
|
+
if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
|
|
381
508
|
if (!isConsulting || !consultInitiator) return DISABLED;
|
|
382
509
|
if (!customerPresent) return VISIBLE_DISABLED;
|
|
383
510
|
if (consultLegOnHold) return VISIBLE_DISABLED;
|
|
@@ -387,6 +514,7 @@ function computeVoiceInteractionUIControls(
|
|
|
387
514
|
|
|
388
515
|
// Switch: visible only on the currently active leg
|
|
389
516
|
switch: (() => {
|
|
517
|
+
if (isHydratedConferenceConsultPending && currentLeg === 'consult') return VISIBLE_DISABLED;
|
|
390
518
|
if (!customerPresent && hasParallelConsultLeg) return DISABLED;
|
|
391
519
|
if (currentLeg === 'consult') {
|
|
392
520
|
if (!isConsulting || !consultInitiator || consultCallHeld) return DISABLED;
|
|
@@ -470,10 +598,18 @@ function getVoiceLegState(
|
|
|
470
598
|
taskData?.consultMediaResourceId ||
|
|
471
599
|
Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
|
|
472
600
|
);
|
|
601
|
+
const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
|
|
602
|
+
const selfConsultPendingOnConsultMedia =
|
|
603
|
+
selfParticipant?.consultState === 'consultInitiated' &&
|
|
604
|
+
!taskData?.isConsulted &&
|
|
605
|
+
hasConsultMedia;
|
|
606
|
+
const selfConsultingOnConsultMedia =
|
|
607
|
+
selfParticipant?.consultState === 'consulting' && hasConsultMedia;
|
|
473
608
|
const hasConsultLeg = Boolean(
|
|
474
|
-
|
|
475
|
-
!taskData?.isConsulted
|
|
476
|
-
|
|
609
|
+
!interaction?.isTerminated &&
|
|
610
|
+
((consultOwnedBySelf && !taskData?.isConsulted) ||
|
|
611
|
+
selfConsultingOnConsultMedia ||
|
|
612
|
+
selfConsultPendingOnConsultMedia) &&
|
|
477
613
|
(consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
|
|
478
614
|
);
|
|
479
615
|
|
|
@@ -486,10 +622,17 @@ function getVoiceLegState(
|
|
|
486
622
|
};
|
|
487
623
|
}
|
|
488
624
|
|
|
625
|
+
let mainState = TaskState.HELD;
|
|
626
|
+
if (currentState === TaskState.CONFERENCING) {
|
|
627
|
+
mainState = TaskState.CONFERENCING;
|
|
628
|
+
} else if (context.consultCallHeld) {
|
|
629
|
+
mainState = TaskState.CONNECTED;
|
|
630
|
+
}
|
|
631
|
+
|
|
489
632
|
return {
|
|
490
633
|
hasConsultLeg: true,
|
|
491
634
|
activeLeg: context.consultCallHeld ? 'main' : 'consult',
|
|
492
|
-
mainState
|
|
635
|
+
mainState,
|
|
493
636
|
consultState: isConsultingState ? currentState : TaskState.CONSULTING,
|
|
494
637
|
};
|
|
495
638
|
}
|
|
@@ -25,7 +25,7 @@ import Task from '../Task';
|
|
|
25
25
|
import LoggerProxy from '../../../logger-proxy';
|
|
26
26
|
import MetricsManager from '../../../metrics/MetricsManager';
|
|
27
27
|
import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
|
|
28
|
-
import {TaskState, TaskEvent} from '../state-machine';
|
|
28
|
+
import {TaskState, TaskEvent, TaskActionArgs} from '../state-machine';
|
|
29
29
|
import {WrapupData} from '../../config/types';
|
|
30
30
|
import {getConsultMediaResourceId, getIsConferenceInProgress} from '../TaskUtils';
|
|
31
31
|
|
|
@@ -150,7 +150,10 @@ export default class Voice extends Task implements IVoice {
|
|
|
150
150
|
});
|
|
151
151
|
throw error;
|
|
152
152
|
}
|
|
153
|
-
} else if (
|
|
153
|
+
} else if (
|
|
154
|
+
!state.matches(TaskState.HELD) &&
|
|
155
|
+
!(state.matches(TaskState.CONFERENCING) && mediaHoldState === true)
|
|
156
|
+
) {
|
|
154
157
|
const error = new Error(`Cannot resume call in current state: ${currentState}`);
|
|
155
158
|
LoggerProxy.error('Resume operation not allowed', {
|
|
156
159
|
module: CC_FILE,
|
|
@@ -1252,9 +1255,13 @@ export default class Voice extends Task implements IVoice {
|
|
|
1252
1255
|
TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED,
|
|
1253
1256
|
{updateTaskData: true}
|
|
1254
1257
|
),
|
|
1255
|
-
emitTaskOutdialFailed:
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
+
emitTaskOutdialFailed: ({event}: TaskActionArgs) => {
|
|
1259
|
+
if (event && 'taskData' in event && event.taskData) {
|
|
1260
|
+
this.updateTaskData(event.taskData as TaskData);
|
|
1261
|
+
}
|
|
1262
|
+
const reason = (event as {reason?: string})?.reason || 'Outdial failed';
|
|
1263
|
+
this.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, reason);
|
|
1264
|
+
},
|
|
1258
1265
|
};
|
|
1259
1266
|
}
|
|
1260
1267
|
}
|
|
@@ -200,11 +200,17 @@ describe('WebCallingService', () => {
|
|
|
200
200
|
|
|
201
201
|
it('should reject if registration times out', async () => {
|
|
202
202
|
line = callingClient.getLines().line1 as ILine;
|
|
203
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(((handler: TimerHandler) => {
|
|
204
|
+
if (typeof handler === 'function') {
|
|
205
|
+
handler();
|
|
206
|
+
}
|
|
207
|
+
return 0 as unknown as NodeJS.Timeout;
|
|
208
|
+
}) as typeof setTimeout);
|
|
203
209
|
|
|
204
210
|
const promise = webRTCCalling.registerWebCallingLine();
|
|
205
211
|
|
|
206
212
|
await expect(promise).rejects.toThrow('WebCallingService Registration timed out');
|
|
207
|
-
}
|
|
213
|
+
});
|
|
208
214
|
|
|
209
215
|
it('should handle incoming calls', async () => {
|
|
210
216
|
line = callingClient.getLines().line1 as ILine;
|
|
@@ -1280,6 +1280,32 @@ describe('TaskManager', () => {
|
|
|
1280
1280
|
sendStateMachineEventSpy.mockRestore();
|
|
1281
1281
|
});
|
|
1282
1282
|
|
|
1283
|
+
it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
|
|
1284
|
+
const task = taskManager.getTask(taskId);
|
|
1285
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1286
|
+
const payload = {
|
|
1287
|
+
data: {
|
|
1288
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1289
|
+
interactionId: taskId,
|
|
1290
|
+
reason: 'CUSTOMER_BUSY',
|
|
1291
|
+
agentsPendingWrapUp: ['agent-123'],
|
|
1292
|
+
interaction: {
|
|
1293
|
+
outboundType: 'OUTDIAL',
|
|
1294
|
+
isTerminated: true,
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1299
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1300
|
+
sendStateMachineEventSpy,
|
|
1301
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1302
|
+
);
|
|
1303
|
+
expect(stateMachineEvent?.taskData).toBeDefined();
|
|
1304
|
+
expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
|
|
1305
|
+
expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
|
|
1306
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1283
1309
|
it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
|
|
1284
1310
|
const payload = {
|
|
1285
1311
|
data: {
|
|
@@ -450,6 +450,14 @@ describe('TaskUtils', () => {
|
|
|
450
450
|
expect(getIsCustomerInCall(interaction, interactionId)).toBe(true);
|
|
451
451
|
});
|
|
452
452
|
|
|
453
|
+
it('getIsCustomerInCall returns false when participants map is missing', () => {
|
|
454
|
+
const interaction = createInteraction(
|
|
455
|
+
{[interactionId]: {mType: 'mainCall', participants: ['c1']}},
|
|
456
|
+
undefined
|
|
457
|
+
);
|
|
458
|
+
expect(getIsCustomerInCall(interaction, interactionId)).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
|
|
453
461
|
it('getConferenceParticipantsCount counts active agents only', () => {
|
|
454
462
|
const interaction = createInteraction(
|
|
455
463
|
{[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
|
|
@@ -458,6 +466,14 @@ describe('TaskUtils', () => {
|
|
|
458
466
|
expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(2);
|
|
459
467
|
});
|
|
460
468
|
|
|
469
|
+
it('getConferenceParticipantsCount returns 0 when participants map is missing', () => {
|
|
470
|
+
const interaction = createInteraction(
|
|
471
|
+
{[interactionId]: {mType: 'mainCall', participants: ['a1', 'a2', 'c1']}},
|
|
472
|
+
undefined
|
|
473
|
+
);
|
|
474
|
+
expect(getConferenceParticipantsCount(interaction, interactionId)).toBe(0);
|
|
475
|
+
});
|
|
476
|
+
|
|
461
477
|
it('isSecondaryAgent returns true for consult with parentInteractionId', () => {
|
|
462
478
|
const interaction = createInteraction();
|
|
463
479
|
interaction.callProcessingDetails = {relationshipType: 'consult', parentInteractionId: 'parent-456'};
|