Skip to content

Commit 40edfe2

Browse files
authored
fix(contact-center): fixed-outdial-fail-issue (#4579)
1 parent 61e8218 commit 40edfe2

File tree

5 files changed

+162
-11
lines changed

5 files changed

+162
-11
lines changed

docs/samples/contact-center/app.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@ function registerTaskListeners(task) {
10131013
showAgentStatePopup(reason);
10141014
});
10151015

1016+
task.on('task:outdialFailed', (reason) => {
1017+
updateTaskList();
1018+
console.info('Outdial failed with reason:', reason);
1019+
showOutdialFailedPopup(reason);
1020+
});
1021+
10161022
task.on('task:wrappedup', updateTaskList); // Update the task list UI to have latest tasks
10171023

10181024
// Conference event listeners - Simplified approach
@@ -1819,6 +1825,31 @@ function showAgentStatePopup(reason) {
18191825
popup.classList.remove('hidden');
18201826
}
18211827

1828+
function showOutdialFailedPopup(reason) {
1829+
const outdialFailedReasonText = document.getElementById('outdialFailedReasonText');
1830+
1831+
// Set the reason text based on the reason
1832+
if (reason === 'CUSTOMER_BUSY') {
1833+
outdialFailedReasonText.innerText = 'Customer is busy';
1834+
} else if (reason === 'NO_ANSWER') {
1835+
outdialFailedReasonText.innerText = 'No answer from customer';
1836+
} else if (reason === 'CALL_FAILED') {
1837+
outdialFailedReasonText.innerText = 'Call failed';
1838+
} else if (reason === 'INVALID_NUMBER') {
1839+
outdialFailedReasonText.innerText = 'Invalid phone number';
1840+
} else {
1841+
outdialFailedReasonText.innerText = `Outdial failed: ${reason}`;
1842+
}
1843+
1844+
const outdialFailedPopup = document.getElementById('outdialFailedPopup');
1845+
outdialFailedPopup.classList.remove('hidden');
1846+
}
1847+
1848+
function closeOutdialFailedPopup() {
1849+
const outdialFailedPopup = document.getElementById('outdialFailedPopup');
1850+
outdialFailedPopup.classList.add('hidden');
1851+
}
1852+
18221853
async function renderBuddyAgents() {
18231854
buddyAgentsDropdownElm.innerHTML = ''; // Clear previous options
18241855
const buddyAgentsDropdownNodes = await fetchBuddyAgentsNodeList();

docs/samples/contact-center/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,15 @@ <h2>Set the state of the agent</h2>
311311
</div>
312312
</div>
313313

314+
<!-- Popup for outdial failed -->
315+
<div id="outdialFailedPopup" class="modal hidden">
316+
<div class="modal-content">
317+
<h2>Outdial Failed</h2>
318+
<p id="outdialFailedReasonText"></p>
319+
<button id="closeOutdialFailedPopup" class="btn btn-primary my-3" onclick="closeOutdialFailedPopup()">Close</button>
320+
</div>
321+
</div>
322+
314323
<style>
315324
.modal {
316325
position: fixed;

packages/@webex/contact-center/src/services/task/TaskManager.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,22 @@ export default class TaskManager extends EventEmitter {
201201
this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);
202202
break;
203203
case CC_EVENTS.AGENT_OUTBOUND_FAILED:
204-
// We don't have to emit any event here since this will be result of promise.
205-
if (task.data) {
206-
this.removeTaskFromCollection(task);
207-
}
204+
task = this.updateTaskData(task, payload.data);
205+
this.metricsManager.trackEvent(
206+
METRIC_EVENT_NAMES.TASK_OUTDIAL_FAILED,
207+
{
208+
...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
209+
taskId: payload.data.interactionId,
210+
reason: payload.data.reasonCode || payload.data.reason,
211+
},
212+
['behavioral', 'operational']
213+
);
208214
LoggerProxy.log(`Agent outbound failed for task`, {
209215
module: TASK_MANAGER_FILE,
210216
method: METHODS.REGISTER_TASK_LISTENERS,
211-
interactionId: payload.data?.interactionId,
217+
interactionId: payload.data.interactionId,
212218
});
219+
task.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, payload.data.reason ?? 'UNKNOWN_REASON');
213220
break;
214221
case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
215222
task = this.updateTaskData(task, payload.data);
@@ -553,9 +560,15 @@ export default class TaskManager extends EventEmitter {
553560
this.webCallingService.cleanUpCall();
554561
}
555562

556-
if (task.data.interaction.state === 'new' || isSecondaryEpDnAgent(task.data.interaction)) {
557-
// Only remove tasks in 'new' state or isSecondaryEpDnAgent immediately. For other states,
558-
// retain tasks until they complete wrap-up, unless the task disconnected before being answered.
563+
const isOutdial = task.data.interaction.outboundType === 'OUTDIAL';
564+
const isNew = task.data.interaction.state === 'new';
565+
const isTerminated = task.data.interaction.isTerminated;
566+
567+
// For OUTDIAL: only remove if NOT terminated (user-declined, no wrap-up follows)
568+
// If terminated, keep task for wrap-up flow (CONTACT_ENDED → AGENT_WRAPUP)
569+
// For non-OUTDIAL: remove if state is 'new'
570+
// Always remove if secondary EpDn agent
571+
if ((isNew && !(isOutdial && isTerminated)) || isSecondaryEpDnAgent(task.data.interaction)) {
559572
this.removeTaskFromCollection(task);
560573
}
561574
}

packages/@webex/contact-center/src/services/task/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ export enum TASK_EVENTS {
347347
*/
348348
TASK_REJECT = 'task:rejected',
349349

350+
/**
351+
* Triggered when an outdial call fails
352+
* @example
353+
* ```typescript
354+
* task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, (reason: string) => {
355+
* console.log('Outdial failed:', reason);
356+
* // Handle outdial failure
357+
* });
358+
* ```
359+
*/
360+
TASK_OUTDIAL_FAILED = 'task:outdialFailed',
361+
350362
/**
351363
* Triggered when a task is populated with data
352364
* @example

packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -673,29 +673,115 @@ describe('TaskManager', () => {
673673
expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
674674
});
675675

676-
it('should remove currentTask from taskCollection on AGENT_OUTBOUND_FAILED event', () => {
676+
it('should NOT remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated (wrap-up flow)', () => {
677+
const task = taskManager.getTask(taskId);
678+
task.updateTaskData = jest.fn().mockImplementation((newData) => {
679+
task.data = {
680+
...task.data,
681+
...newData,
682+
interaction: {
683+
...task.data.interaction,
684+
...newData.interaction,
685+
outboundType: 'OUTDIAL',
686+
state: 'new',
687+
isTerminated: true,
688+
},
689+
};
690+
return task;
691+
});
692+
task.unregisterWebCallListeners = jest.fn();
693+
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
694+
677695
const payload = {
678696
data: {
679697
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
680698
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
681699
eventTime: 1733211616959,
682700
eventType: 'RoutingMessage',
683-
interaction: {},
701+
interaction: {
702+
outboundType: 'OUTDIAL',
703+
state: 'new',
704+
isTerminated: true,
705+
},
684706
interactionId: taskId,
685707
orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
686708
trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
687709
mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
688710
destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
689711
owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
690712
queueMgr: 'aqm',
713+
reason: 'CUSTOMER_BUSY',
714+
reasonCode: 1022,
691715
},
692716
};
693717

694-
taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
718+
webSocketManagerMock.emit('message', JSON.stringify(payload));
719+
720+
expect(taskManager.getTask(taskId)).toBeDefined();
721+
expect(removeTaskSpy).not.toHaveBeenCalled();
722+
});
723+
724+
it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
725+
const task = taskManager.getTask(taskId);
726+
task.updateTaskData = jest.fn().mockReturnValue(task);
727+
const taskEmitSpy = jest.spyOn(task, 'emit');
728+
const payload = {
729+
data: {
730+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
731+
interactionId: taskId,
732+
reason: 'CUSTOMER_BUSY',
733+
},
734+
};
735+
webSocketManagerMock.emit('message', JSON.stringify(payload));
736+
expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
737+
});
738+
739+
it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
740+
const task = taskManager.getTask(taskId);
741+
task.updateTaskData = jest.fn().mockImplementation((newData) => {
742+
task.data = {
743+
...task.data,
744+
...newData,
745+
interaction: {
746+
...task.data.interaction,
747+
...newData.interaction,
748+
outboundType: 'OUTDIAL',
749+
state: 'new',
750+
isTerminated: false,
751+
},
752+
};
753+
return task;
754+
});
755+
task.unregisterWebCallListeners = jest.fn();
756+
const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
757+
758+
const payload = {
759+
data: {
760+
type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
761+
agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
762+
eventTime: 1733211616959,
763+
eventType: 'RoutingMessage',
764+
interaction: {
765+
outboundType: 'OUTDIAL',
766+
state: 'new',
767+
isTerminated: false,
768+
},
769+
interactionId: taskId,
770+
orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
771+
trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
772+
mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
773+
destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
774+
owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
775+
queueMgr: 'aqm',
776+
reason: 'USER_DECLINED',
777+
reasonCode: 156,
778+
},
779+
};
695780

696781
webSocketManagerMock.emit('message', JSON.stringify(payload));
697782

698783
expect(taskManager.getTask(taskId)).toBeUndefined();
784+
expect(removeTaskSpy).toHaveBeenCalled();
699785
});
700786

701787
it('handle AGENT_OFFER_CONSULT event', () => {

0 commit comments

Comments
 (0)