From 711be1f31907eeb8b32525c1ced07d100e6f3dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Fri, 14 Nov 2025 19:06:22 +0530 Subject: [PATCH 1/5] fix(handle-auto-answer): implement-auto-answer-for-desktop-calls --- docs/samples/contact-center/app.js | 2 + .../src/services/config/types.ts | 2 + .../src/services/task/TaskManager.ts | 65 +++++ .../src/services/task/TaskUtils.ts | 94 ++++++ .../contact-center/src/services/task/types.ts | 14 + .../unit/spec/services/task/TaskManager.ts | 31 ++ .../test/unit/spec/services/task/TaskUtils.ts | 276 +++++++++++++++++- 7 files changed, 481 insertions(+), 3 deletions(-) diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index af640440959..adeac8a8adf 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -1000,6 +1000,8 @@ function registerTaskListeners(task) { task.on('task:offerConsult', updateTaskList); + task.on('task:offerCampaignReservation', updateTaskList); + task.on('task:consultAccepted', updateTaskList); task.on('task:consulting', updateTaskList); diff --git a/packages/@webex/contact-center/src/services/config/types.ts b/packages/@webex/contact-center/src/services/config/types.ts index 8ab9e4c961e..81ee07503a8 100644 --- a/packages/@webex/contact-center/src/services/config/types.ts +++ b/packages/@webex/contact-center/src/services/config/types.ts @@ -105,6 +105,8 @@ export const CC_TASK_EVENTS = { AGENT_CONTACT: 'AgentContact', /** Event emitted when contact is offered to agent */ AGENT_OFFER_CONTACT: 'AgentOfferContact', + /** Event emitted when campaign reservation is offered to agent */ + AGENT_OFFER_CAMPAIGN_RESERVATION: 'AgentOfferCampaignReservation', /** Event emitted when contact is assigned to agent */ AGENT_CONTACT_ASSIGNED: 'AgentContactAssigned', /** Event emitted when contact is unassigned from agent */ diff --git a/packages/@webex/contact-center/src/services/task/TaskManager.ts b/packages/@webex/contact-center/src/services/task/TaskManager.ts index 35db7ce8e56..a9ee3d90072 100644 --- a/packages/@webex/contact-center/src/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/src/services/task/TaskManager.ts @@ -18,6 +18,7 @@ import { isParticipantInMainInteraction, isPrimary, isSecondaryEpDnAgent, + shouldAutoAnswerTask, } from './TaskUtils'; /** @internal */ @@ -190,6 +191,18 @@ export default class TaskManager extends EventEmitter { this.emit(TASK_EVENTS.TASK_INCOMING, task); } break; + case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: + task = this.updateTaskData(task, payload.data); + LoggerProxy.log(`Agent offer campaign reservation received for task`, { + module: TASK_MANAGER_FILE, + method: METHODS.REGISTER_TASK_LISTENERS, + interactionId: payload.data?.interactionId, + }); + this.emit(TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, task); + + // Handle auto-answer for campaign reservation + this.handleAutoAnswer(task); + break; case CC_EVENTS.AGENT_OFFER_CONTACT: // We don't have to emit any event here since this will be result of promise. task = this.updateTaskData(task, payload.data); @@ -199,6 +212,9 @@ export default class TaskManager extends EventEmitter { interactionId: payload.data?.interactionId, }); this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task); + + // Handle auto-answer for offer contact + this.handleAutoAnswer(task); break; case CC_EVENTS.AGENT_OUTBOUND_FAILED: // We don't have to emit any event here since this will be result of promise. @@ -301,6 +317,9 @@ export default class TaskManager extends EventEmitter { isConsulted: true, // This ensures that the task is marked as us being requested for a consult }); task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task); + + // Handle auto-answer for consult offer + this.handleAutoAnswer(task); break; case CC_EVENTS.AGENT_CONSULTING: // Received when agent is in an active consult state @@ -538,6 +557,52 @@ export default class TaskManager extends EventEmitter { } } + /** + * Handles auto-answer logic for incoming tasks + * Automatically accepts tasks when certain conditions are met: + * 1. WebRTC calls with auto-answer enabled in agent profile + * 2. Agent-initiated WebRTC outdial calls + * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers + * + * @param task - The task to evaluate for auto-answer + * @private + */ + private async handleAutoAnswer(task: ITask): Promise { + if (!task || !task.data) { + return; + } + + const shouldAutoAnswer = shouldAutoAnswerTask( + task.data, + this.agentId, + this.webCallingService.loginOption + ); + + if (shouldAutoAnswer) { + LoggerProxy.info(`Auto-answering task`, { + module: TASK_MANAGER_FILE, + method: 'handleAutoAnswer', + interactionId: task.data.interactionId, + }); + + try { + await task.accept(); + LoggerProxy.info(`Task auto-answered successfully`, { + module: TASK_MANAGER_FILE, + method: 'handleAutoAnswer', + interactionId: task.data.interactionId, + }); + } catch (error) { + LoggerProxy.error(`Failed to auto-answer task`, { + module: TASK_MANAGER_FILE, + method: 'handleAutoAnswer', + interactionId: task.data.interactionId, + error, + }); + } + } + } + /** * Handles cleanup of task resources including Desktop/WebRTC call cleanup and task removal * @param task - The task to clean up diff --git a/packages/@webex/contact-center/src/services/task/TaskUtils.ts b/packages/@webex/contact-center/src/services/task/TaskUtils.ts index e59fd5b488e..40a1424de1a 100644 --- a/packages/@webex/contact-center/src/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/src/services/task/TaskUtils.ts @@ -111,3 +111,97 @@ export const isSecondaryEpDnAgent = (interaction: Interaction): boolean => { return interaction.mediaType === 'telephony' && isSecondaryAgent(interaction); }; + +/** + * Checks if auto-answer is enabled for the agent participant + * @param interaction - The interaction object + * @param agentId - Current agent ID + * @returns true if auto-answer is enabled, false otherwise + */ +export const isAutoAnswerEnabled = (interaction: Interaction, agentId: string): boolean => { + return interaction.participants?.[agentId]?.autoAnswerEnabled === true; +}; + +/** + * Checks if the interaction is a WebRTC call eligible for auto-answer + * @param interaction - The interaction object + * @param loginOption - The agent's login option (BROWSER, AGENT_DN, etc.) + * @returns true if this is a WebRTC call, false otherwise + */ +export const isWebRTCCall = (interaction: Interaction, loginOption: string): boolean => { + return loginOption === 'BROWSER' && interaction.mediaType === 'telephony'; +}; + +/** + * Checks if the interaction is a digital outbound (Email/SMS) + * @param interaction - The interaction object + * @returns true if this is a digital outbound, false otherwise + */ +export const isDigitalOutbound = (interaction: Interaction): boolean => { + return ( + interaction.contactDirection?.type === 'OUTBOUND' && + interaction.outboundType === 'OUTDIAL' && + (interaction.mediaChannel === 'email' || interaction.mediaChannel === 'sms') + ); +}; + +/** + * Checks if the outdial was initiated by the current agent + * @param interaction - The interaction object + * @param agentId - Current agent ID + * @returns true if agent initiated the outdial, false otherwise + */ +export const isAgentInitiatedOutdial = (interaction: Interaction, agentId: string): boolean => { + return ( + interaction.contactDirection?.type === 'OUTBOUND' && + interaction.outboundType === 'OUTDIAL' && + interaction.callProcessingDetails?.outdialAgentId === agentId && + interaction.owner === agentId && + !interaction.callProcessingDetails?.BLIND_TRANSFER_IN_PROGRESS + ); +}; + +/** + * Determines if a task should be auto-answered based on interaction data + * Auto-answer logic handles: + * 1. WebRTC calls with auto-answer enabled in agent profile + * 2. Agent-initiated WebRTC outdial calls + * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers + * + * @param taskData - The task data + * @param agentId - Current agent ID + * @param loginOption - Agent's login option + * @returns true if task should be auto-answered, false otherwise + */ +export const shouldAutoAnswerTask = ( + taskData: TaskData, + agentId: string, + loginOption: string +): boolean => { + const {interaction} = taskData; + + if (!interaction || !agentId) { + return false; + } + + // Check if auto-answer is enabled for this agent + const autoAnswerEnabled = isAutoAnswerEnabled(interaction, agentId); + + // Check if this is an agent-initiated outdial + const agentInitiatedOutdial = isAgentInitiatedOutdial(interaction, agentId); + + // WebRTC telephony calls + if (isWebRTCCall(interaction, loginOption)) { + return autoAnswerEnabled || agentInitiatedOutdial; + } + + // Digital outbound (Email/SMS) + if (isDigitalOutbound(interaction) && agentInitiatedOutdial) { + // Don't auto-answer if task has been transferred (has previous vteams) + const hasPreviousVteams = interaction.previousVTeams && interaction.previousVTeams.length > 0; + + return !hasPreviousVteams; + } + + return false; +}; diff --git a/packages/@webex/contact-center/src/services/task/types.ts b/packages/@webex/contact-center/src/services/task/types.ts index 2f461ab20f9..b293b33a877 100644 --- a/packages/@webex/contact-center/src/services/task/types.ts +++ b/packages/@webex/contact-center/src/services/task/types.ts @@ -371,6 +371,18 @@ export enum TASK_EVENTS { */ TASK_OFFER_CONTACT = 'task:offerContact', + /** + * Triggered when a campaign reservation is offered to agent + * @example + * ```typescript + * task.on(TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, (task: ITask) => { + * console.log('Campaign reservation offered:', task.data.interactionId); + * // Handle campaign reservation offer + * }); + * ``` + */ + TASK_OFFER_CAMPAIGN_RESERVATION = 'task:offerCampaignReservation', + /** * Triggered when a conference is being established * @example @@ -660,6 +672,8 @@ export type Interaction = { BLIND_TRANSFER_IN_PROGRESS?: boolean; /** Desktop view configuration for Flow Control */ fcDesktopView?: string; + /** Agent ID who initiated the outdial call */ + outdialAgentId?: string; }; /** Main interaction identifier for related interactions */ mainInteractionId?: string; diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts b/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts index 08cd5c39f47..401a695c655 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts @@ -2105,5 +2105,36 @@ describe('TaskManager', () => { ); }); }); + + describe('Auto-Answer Functionality', () => { + // Note: The auto-answer helper functions are thoroughly tested in TaskUtils.ts + + it('should handle AGENT_OFFER_CAMPAIGN_RESERVATION event', () => { + const campaignPayload = { + data: { + ...initalPayload.data, + type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION, + }, + }; + + // Create initial task + webSocketManagerMock.emit('message', JSON.stringify(initalPayload)); + + const managerEmitSpy = jest.spyOn(taskManager, 'emit'); + + // Emit campaign reservation event + webSocketManagerMock.emit('message', JSON.stringify(campaignPayload)); + + // Verify the manager emitted the new TASK_OFFER_CAMPAIGN_RESERVATION event + expect(managerEmitSpy).toHaveBeenCalledWith( + TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, + expect.objectContaining({ + data: expect.objectContaining({ + interactionId: taskId + }) + }) + ); + }); + }); }); diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts index aebfc02a988..c727b78398e 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts @@ -1,8 +1,15 @@ -import { checkParticipantNotInInteraction, +import { + checkParticipantNotInInteraction, getIsConferenceInProgress, isParticipantInMainInteraction, - isPrimary,} from '../../../../../src/services/task/TaskUtils'; -import {ITask} from '../../../../../src/services/task/types'; + isPrimary, + isAutoAnswerEnabled, + isWebRTCCall, + isDigitalOutbound, + isAgentInitiatedOutdial, + shouldAutoAnswerTask, +} from '../../../../../src/services/task/TaskUtils'; +import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types'; describe('TaskUtils', () => { let mockTask: ITask; @@ -128,4 +135,267 @@ describe('TaskUtils', () => { expect(getIsConferenceInProgress(mockTask.data)).toBe(false); }); }); + + describe('Auto-Answer Helper Functions', () => { + let mockInteraction: Interaction; + + beforeEach(() => { + mockInteraction = { + interactionId: 'interaction-123', + mediaType: 'telephony', + mediaChannel: 'telephony', + participants: { + [mockAgentId]: { + id: mockAgentId, + pType: 'Agent', + autoAnswerEnabled: false, + hasJoined: false, + hasLeft: false, + }, + }, + owner: mockAgentId, + contactDirection: {type: 'INBOUND'}, + outboundType: null, + callProcessingDetails: { + QMgrName: 'aqm', + taskToBeSelfServiced: 'false', + ani: '+1234567890', + displayAni: '+1234567890', + dnis: '+0987654321', + tenantId: 'tenant-123', + QueueId: 'queue-123', + vteamId: 'vteam-123', + customerName: 'Test Customer', + virtualTeamName: 'Test Team', + ronaTimeout: '30', + category: 'Support', + reason: 'General', + sourceNumber: '+1234567890', + sourcePage: 'web', + appUser: 'test-app', + customerNumber: '+1234567890', + reasonCode: '100', + IvrPath: '/ivr/path', + pathId: 'path-123', + fromAddress: 'customer@example.com', + }, + previousVTeams: [], + state: 'new', + currentVTeam: 'vteam-123', + isFcManaged: false, + isTerminated: false, + orgId: 'org-123', + createdTimestamp: Date.now(), + media: {}, + } as any; + }); + + describe('isAutoAnswerEnabled', () => { + it('should return true when autoAnswerEnabled is true', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; + expect(isAutoAnswerEnabled(mockInteraction, mockAgentId)).toBe(true); + }); + + it('should return false when autoAnswerEnabled is false', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = false; + expect(isAutoAnswerEnabled(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when autoAnswerEnabled is not set', () => { + delete mockInteraction.participants[mockAgentId].autoAnswerEnabled; + expect(isAutoAnswerEnabled(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when participant does not exist', () => { + expect(isAutoAnswerEnabled(mockInteraction, 'non-existent-agent')).toBe(false); + }); + }); + + describe('isWebRTCCall', () => { + it('should return true for BROWSER login with telephony media type', () => { + expect(isWebRTCCall(mockInteraction, 'BROWSER')).toBe(true); + }); + + it('should return false for AGENT_DN login', () => { + expect(isWebRTCCall(mockInteraction, 'AGENT_DN')).toBe(false); + }); + + it('should return false for EXTENSION login', () => { + expect(isWebRTCCall(mockInteraction, 'EXTENSION')).toBe(false); + }); + + it('should return false for BROWSER login with non-telephony media type', () => { + mockInteraction.mediaType = 'email'; + expect(isWebRTCCall(mockInteraction, 'BROWSER')).toBe(false); + }); + }); + + describe('isDigitalOutbound', () => { + it('should return true for email outdial', () => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.mediaChannel = 'email'; + expect(isDigitalOutbound(mockInteraction)).toBe(true); + }); + + it('should return true for SMS outdial', () => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.mediaChannel = 'sms'; + expect(isDigitalOutbound(mockInteraction)).toBe(true); + }); + + it('should return false for telephony outdial', () => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.mediaChannel = 'telephony'; + expect(isDigitalOutbound(mockInteraction)).toBe(false); + }); + + it('should return false for inbound email', () => { + mockInteraction.contactDirection = {type: 'INBOUND'}; + mockInteraction.mediaChannel = 'email'; + expect(isDigitalOutbound(mockInteraction)).toBe(false); + }); + + it('should return false when outboundType is not OUTDIAL', () => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'CALLBACK'; + mockInteraction.mediaChannel = 'email'; + expect(isDigitalOutbound(mockInteraction)).toBe(false); + }); + }); + + describe('isAgentInitiatedOutdial', () => { + beforeEach(() => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.owner = mockAgentId; + mockInteraction.callProcessingDetails.outdialAgentId = mockAgentId; + mockInteraction.callProcessingDetails.BLIND_TRANSFER_IN_PROGRESS = false; + }); + + it('should return true for agent-initiated outdial', () => { + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(true); + }); + + it('should return false when not outbound', () => { + mockInteraction.contactDirection = {type: 'INBOUND'}; + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when not OUTDIAL type', () => { + mockInteraction.outboundType = 'CALLBACK'; + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when outdialAgentId does not match', () => { + mockInteraction.callProcessingDetails.outdialAgentId = mockOtherAgentId; + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when owner does not match', () => { + mockInteraction.owner = mockOtherAgentId; + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(false); + }); + + it('should return false when blind transfer is in progress', () => { + mockInteraction.callProcessingDetails.BLIND_TRANSFER_IN_PROGRESS = true; + expect(isAgentInitiatedOutdial(mockInteraction, mockAgentId)).toBe(false); + }); + }); + + describe('shouldAutoAnswerTask', () => { + let mockTaskData: TaskData; + + beforeEach(() => { + mockTaskData = { + interactionId: 'interaction-123', + agentId: mockAgentId, + interaction: mockInteraction, + } as any; + }); + + describe('WebRTC scenarios', () => { + beforeEach(() => { + mockInteraction.mediaType = 'telephony'; + }); + + it('should return true when auto-answer is enabled for WebRTC call', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + }); + + it('should return true for agent-initiated WebRTC outdial', () => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.owner = mockAgentId; + mockInteraction.callProcessingDetails.outdialAgentId = mockAgentId; + mockInteraction.callProcessingDetails.BLIND_TRANSFER_IN_PROGRESS = false; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + }); + + it('should return false for WebRTC call without auto-answer or outdial', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = false; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + }); + + it('should return false for non-BROWSER login', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'AGENT_DN')).toBe(false); + }); + }); + + describe('Digital outbound scenarios', () => { + beforeEach(() => { + mockInteraction.contactDirection = {type: 'OUTBOUND'}; + mockInteraction.outboundType = 'OUTDIAL'; + mockInteraction.owner = mockAgentId; + mockInteraction.callProcessingDetails.outdialAgentId = mockAgentId; + mockInteraction.previousVTeams = []; + }); + + it('should return true for agent-initiated email outdial', () => { + mockInteraction.mediaType = 'email'; + mockInteraction.mediaChannel = 'email'; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + }); + + it('should return true for agent-initiated SMS outdial', () => { + mockInteraction.mediaType = 'sms'; + mockInteraction.mediaChannel = 'sms'; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + }); + + it('should return false when digital outbound has previous vteams', () => { + mockInteraction.mediaType = 'email'; + mockInteraction.mediaChannel = 'email'; + mockInteraction.previousVTeams = ['vteam-1']; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + }); + + it('should return false when digital outbound is not agent-initiated', () => { + mockInteraction.mediaType = 'email'; + mockInteraction.mediaChannel = 'email'; + mockInteraction.owner = mockOtherAgentId; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + }); + }); + + describe('Edge cases', () => { + it('should return false when interaction is null', () => { + mockTaskData.interaction = null as any; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + }); + + it('should return false when agentId is null', () => { + expect(shouldAutoAnswerTask(mockTaskData, null as any, 'BROWSER')).toBe(false); + }); + + it('should return false when agentId is empty string', () => { + expect(shouldAutoAnswerTask(mockTaskData, '', 'BROWSER')).toBe(false); + }); + }); + }); + }); }); From c0a934e34279fab151e86bf8076abf63563e8808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Mon, 17 Nov 2025 13:04:35 +0530 Subject: [PATCH 2/5] fix(handle-auto-answer): added-webrtcenabled-feature-toggle-check --- packages/@webex/contact-center/src/cc.ts | 1 + .../src/services/task/TaskManager.ts | 8 +++- .../src/services/task/TaskUtils.ts | 15 +++++-- .../contact-center/test/unit/spec/cc.ts | 1 + .../test/unit/spec/services/task/TaskUtils.ts | 41 +++++++++++-------- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/@webex/contact-center/src/cc.ts b/packages/@webex/contact-center/src/cc.ts index 8cbe43fb0aa..889f8f77d8f 100644 --- a/packages/@webex/contact-center/src/cc.ts +++ b/packages/@webex/contact-center/src/cc.ts @@ -709,6 +709,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter // TODO: Make profile a singleton to make it available throughout app/sdk so we dont need to inject info everywhere this.taskManager.setWrapupData(this.agentConfig.wrapUpData); this.taskManager.setAgentId(this.agentConfig.agentId); + this.taskManager.setWebRtcEnabled(this.agentConfig.webRtcEnabled); if ( this.agentConfig.webRtcEnabled && diff --git a/packages/@webex/contact-center/src/services/task/TaskManager.ts b/packages/@webex/contact-center/src/services/task/TaskManager.ts index a9ee3d90072..ad09954865c 100644 --- a/packages/@webex/contact-center/src/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/src/services/task/TaskManager.ts @@ -37,6 +37,7 @@ export default class TaskManager extends EventEmitter { private static taskManager; private wrapupData: WrapupData; private agentId: string; + private webRtcEnabled: boolean; /** * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises * @param webCallingService - Webrtc Service Layer @@ -74,6 +75,10 @@ export default class TaskManager extends EventEmitter { return this.agentId; } + public setWebRtcEnabled(webRtcEnabled: boolean) { + this.webRtcEnabled = webRtcEnabled; + } + private handleIncomingWebCall = (call: ICall) => { const currentTask = Object.values(this.taskCollection).find( (task) => task.data.interaction.mediaType === 'telephony' @@ -575,7 +580,8 @@ export default class TaskManager extends EventEmitter { const shouldAutoAnswer = shouldAutoAnswerTask( task.data, this.agentId, - this.webCallingService.loginOption + this.webCallingService.loginOption, + this.webRtcEnabled ); if (shouldAutoAnswer) { diff --git a/packages/@webex/contact-center/src/services/task/TaskUtils.ts b/packages/@webex/contact-center/src/services/task/TaskUtils.ts index 40a1424de1a..8982b1059e6 100644 --- a/packages/@webex/contact-center/src/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/src/services/task/TaskUtils.ts @@ -126,10 +126,15 @@ export const isAutoAnswerEnabled = (interaction: Interaction, agentId: string): * Checks if the interaction is a WebRTC call eligible for auto-answer * @param interaction - The interaction object * @param loginOption - The agent's login option (BROWSER, AGENT_DN, etc.) + * @param webRtcEnabled - Whether WebRTC is enabled for the agent * @returns true if this is a WebRTC call, false otherwise */ -export const isWebRTCCall = (interaction: Interaction, loginOption: string): boolean => { - return loginOption === 'BROWSER' && interaction.mediaType === 'telephony'; +export const isWebRTCCall = ( + interaction: Interaction, + loginOption: string, + webRtcEnabled: boolean +): boolean => { + return webRtcEnabled && loginOption === 'BROWSER' && interaction.mediaType === 'telephony'; }; /** @@ -171,12 +176,14 @@ export const isAgentInitiatedOutdial = (interaction: Interaction, agentId: strin * @param taskData - The task data * @param agentId - Current agent ID * @param loginOption - Agent's login option + * @param webRtcEnabled - Whether WebRTC is enabled for the agent * @returns true if task should be auto-answered, false otherwise */ export const shouldAutoAnswerTask = ( taskData: TaskData, agentId: string, - loginOption: string + loginOption: string, + webRtcEnabled: boolean ): boolean => { const {interaction} = taskData; @@ -191,7 +198,7 @@ export const shouldAutoAnswerTask = ( const agentInitiatedOutdial = isAgentInitiatedOutdial(interaction, agentId); // WebRTC telephony calls - if (isWebRTCCall(interaction, loginOption)) { + if (isWebRTCCall(interaction, loginOption, webRtcEnabled)) { return autoAnswerEnabled || agentInitiatedOutdial; } diff --git a/packages/@webex/contact-center/test/unit/spec/cc.ts b/packages/@webex/contact-center/test/unit/spec/cc.ts index 953c848bc92..590346bf585 100644 --- a/packages/@webex/contact-center/test/unit/spec/cc.ts +++ b/packages/@webex/contact-center/test/unit/spec/cc.ts @@ -141,6 +141,7 @@ describe('webex.cc', () => { task: undefined, setWrapupData: jest.fn(), setAgentId: jest.fn(), + setWebRtcEnabled: jest.fn(), registerIncomingCallEvent: jest.fn(), registerTaskListeners: jest.fn(), getTask: jest.fn(), diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts index c727b78398e..1eaff1334b9 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts @@ -212,21 +212,25 @@ describe('TaskUtils', () => { }); describe('isWebRTCCall', () => { - it('should return true for BROWSER login with telephony media type', () => { - expect(isWebRTCCall(mockInteraction, 'BROWSER')).toBe(true); + it('should return true for BROWSER login with telephony media type and webRTC enabled', () => { + expect(isWebRTCCall(mockInteraction, 'BROWSER', true)).toBe(true); + }); + + it('should return false when webRTC is disabled', () => { + expect(isWebRTCCall(mockInteraction, 'BROWSER', false)).toBe(false); }); it('should return false for AGENT_DN login', () => { - expect(isWebRTCCall(mockInteraction, 'AGENT_DN')).toBe(false); + expect(isWebRTCCall(mockInteraction, 'AGENT_DN', true)).toBe(false); }); it('should return false for EXTENSION login', () => { - expect(isWebRTCCall(mockInteraction, 'EXTENSION')).toBe(false); + expect(isWebRTCCall(mockInteraction, 'EXTENSION', true)).toBe(false); }); it('should return false for BROWSER login with non-telephony media type', () => { mockInteraction.mediaType = 'email'; - expect(isWebRTCCall(mockInteraction, 'BROWSER')).toBe(false); + expect(isWebRTCCall(mockInteraction, 'BROWSER', true)).toBe(false); }); }); @@ -323,7 +327,7 @@ describe('TaskUtils', () => { it('should return true when auto-answer is enabled for WebRTC call', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); }); it('should return true for agent-initiated WebRTC outdial', () => { @@ -332,17 +336,22 @@ describe('TaskUtils', () => { mockInteraction.owner = mockAgentId; mockInteraction.callProcessingDetails.outdialAgentId = mockAgentId; mockInteraction.callProcessingDetails.BLIND_TRANSFER_IN_PROGRESS = false; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); }); it('should return false for WebRTC call without auto-answer or outdial', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = false; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); + }); + + it('should return false when webRTC is disabled', () => { + mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', false)).toBe(false); }); it('should return false for non-BROWSER login', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'AGENT_DN')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'AGENT_DN', true)).toBe(false); }); }); @@ -358,42 +367,42 @@ describe('TaskUtils', () => { it('should return true for agent-initiated email outdial', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); }); it('should return true for agent-initiated SMS outdial', () => { mockInteraction.mediaType = 'sms'; mockInteraction.mediaChannel = 'sms'; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); }); it('should return false when digital outbound has previous vteams', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; mockInteraction.previousVTeams = ['vteam-1']; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); }); it('should return false when digital outbound is not agent-initiated', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; mockInteraction.owner = mockOtherAgentId; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); }); }); describe('Edge cases', () => { it('should return false when interaction is null', () => { mockTaskData.interaction = null as any; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); }); it('should return false when agentId is null', () => { - expect(shouldAutoAnswerTask(mockTaskData, null as any, 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, null as any, 'BROWSER', true)).toBe(false); }); it('should return false when agentId is empty string', () => { - expect(shouldAutoAnswerTask(mockTaskData, '', 'BROWSER')).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, '', 'BROWSER', true)).toBe(false); }); }); }); From ef43a933ec0379ebc94bb2d9356f9139148a2bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Mon, 17 Nov 2025 13:23:48 +0530 Subject: [PATCH 3/5] fix(handle-auto-answer): replace-hardcoded-strings-with-constants --- .../src/services/task/TaskUtils.ts | 21 ++++--- .../test/unit/spec/services/task/TaskUtils.ts | 57 +++++++++++++------ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/packages/@webex/contact-center/src/services/task/TaskUtils.ts b/packages/@webex/contact-center/src/services/task/TaskUtils.ts index 8982b1059e6..f1c49d38264 100644 --- a/packages/@webex/contact-center/src/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/src/services/task/TaskUtils.ts @@ -1,5 +1,7 @@ /* eslint-disable import/prefer-default-export */ -import {Interaction, ITask, TaskData} from './types'; +import {Interaction, ITask, TaskData, MEDIA_CHANNEL} from './types'; +import {OUTDIAL_DIRECTION, OUTDIAL_MEDIA_TYPE, OUTBOUND_TYPE} from '../../constants'; +import {LoginOption} from '../../types'; /** * Determines if the given agent is the primary agent (owner) of the task @@ -134,7 +136,11 @@ export const isWebRTCCall = ( loginOption: string, webRtcEnabled: boolean ): boolean => { - return webRtcEnabled && loginOption === 'BROWSER' && interaction.mediaType === 'telephony'; + return ( + webRtcEnabled && + loginOption === LoginOption.BROWSER && + interaction.mediaType === OUTDIAL_MEDIA_TYPE + ); }; /** @@ -144,9 +150,10 @@ export const isWebRTCCall = ( */ export const isDigitalOutbound = (interaction: Interaction): boolean => { return ( - interaction.contactDirection?.type === 'OUTBOUND' && - interaction.outboundType === 'OUTDIAL' && - (interaction.mediaChannel === 'email' || interaction.mediaChannel === 'sms') + interaction.contactDirection?.type === OUTDIAL_DIRECTION && + interaction.outboundType === OUTBOUND_TYPE && + (interaction.mediaChannel === MEDIA_CHANNEL.EMAIL || + interaction.mediaChannel === MEDIA_CHANNEL.SMS) ); }; @@ -158,8 +165,8 @@ export const isDigitalOutbound = (interaction: Interaction): boolean => { */ export const isAgentInitiatedOutdial = (interaction: Interaction, agentId: string): boolean => { return ( - interaction.contactDirection?.type === 'OUTBOUND' && - interaction.outboundType === 'OUTDIAL' && + interaction.contactDirection?.type === OUTDIAL_DIRECTION && + interaction.outboundType === OUTBOUND_TYPE && interaction.callProcessingDetails?.outdialAgentId === agentId && interaction.owner === agentId && !interaction.callProcessingDetails?.BLIND_TRANSFER_IN_PROGRESS diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts index 1eaff1334b9..b290f28bd68 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/TaskUtils.ts @@ -10,6 +10,7 @@ import { shouldAutoAnswerTask, } from '../../../../../src/services/task/TaskUtils'; import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types'; +import {LoginOption} from '../../../../../src/types'; describe('TaskUtils', () => { let mockTask: ITask; @@ -213,24 +214,24 @@ describe('TaskUtils', () => { describe('isWebRTCCall', () => { it('should return true for BROWSER login with telephony media type and webRTC enabled', () => { - expect(isWebRTCCall(mockInteraction, 'BROWSER', true)).toBe(true); + expect(isWebRTCCall(mockInteraction, LoginOption.BROWSER, true)).toBe(true); }); it('should return false when webRTC is disabled', () => { - expect(isWebRTCCall(mockInteraction, 'BROWSER', false)).toBe(false); + expect(isWebRTCCall(mockInteraction, LoginOption.BROWSER, false)).toBe(false); }); it('should return false for AGENT_DN login', () => { - expect(isWebRTCCall(mockInteraction, 'AGENT_DN', true)).toBe(false); + expect(isWebRTCCall(mockInteraction, LoginOption.AGENT_DN, true)).toBe(false); }); it('should return false for EXTENSION login', () => { - expect(isWebRTCCall(mockInteraction, 'EXTENSION', true)).toBe(false); + expect(isWebRTCCall(mockInteraction, LoginOption.EXTENSION, true)).toBe(false); }); it('should return false for BROWSER login with non-telephony media type', () => { mockInteraction.mediaType = 'email'; - expect(isWebRTCCall(mockInteraction, 'BROWSER', true)).toBe(false); + expect(isWebRTCCall(mockInteraction, LoginOption.BROWSER, true)).toBe(false); }); }); @@ -327,7 +328,9 @@ describe('TaskUtils', () => { it('should return true when auto-answer is enabled for WebRTC call', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + true + ); }); it('should return true for agent-initiated WebRTC outdial', () => { @@ -336,22 +339,30 @@ describe('TaskUtils', () => { mockInteraction.owner = mockAgentId; mockInteraction.callProcessingDetails.outdialAgentId = mockAgentId; mockInteraction.callProcessingDetails.BLIND_TRANSFER_IN_PROGRESS = false; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + true + ); }); it('should return false for WebRTC call without auto-answer or outdial', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = false; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + false + ); }); it('should return false when webRTC is disabled', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', false)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, false)).toBe( + false + ); }); it('should return false for non-BROWSER login', () => { mockInteraction.participants[mockAgentId].autoAnswerEnabled = true; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'AGENT_DN', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.AGENT_DN, true)).toBe( + false + ); }); }); @@ -367,42 +378,54 @@ describe('TaskUtils', () => { it('should return true for agent-initiated email outdial', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + true + ); }); it('should return true for agent-initiated SMS outdial', () => { mockInteraction.mediaType = 'sms'; mockInteraction.mediaChannel = 'sms'; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(true); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + true + ); }); it('should return false when digital outbound has previous vteams', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; mockInteraction.previousVTeams = ['vteam-1']; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + false + ); }); it('should return false when digital outbound is not agent-initiated', () => { mockInteraction.mediaType = 'email'; mockInteraction.mediaChannel = 'email'; mockInteraction.owner = mockOtherAgentId; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + false + ); }); }); describe('Edge cases', () => { it('should return false when interaction is null', () => { mockTaskData.interaction = null as any; - expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, mockAgentId, LoginOption.BROWSER, true)).toBe( + false + ); }); it('should return false when agentId is null', () => { - expect(shouldAutoAnswerTask(mockTaskData, null as any, 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, null as any, LoginOption.BROWSER, true)).toBe( + false + ); }); it('should return false when agentId is empty string', () => { - expect(shouldAutoAnswerTask(mockTaskData, '', 'BROWSER', true)).toBe(false); + expect(shouldAutoAnswerTask(mockTaskData, '', LoginOption.BROWSER, true)).toBe(false); }); }); }); From e0c2929875a109737eac5745c666ec4fa94dc281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Tue, 18 Nov 2025 12:51:12 +0530 Subject: [PATCH 4/5] fix(handle-auto-answer): clean-up-the-code --- docs/samples/contact-center/app.js | 2 -- .../src/services/config/types.ts | 2 -- .../src/services/task/TaskManager.ts | 12 -------- .../contact-center/src/services/task/types.ts | 12 -------- .../unit/spec/services/task/TaskManager.ts | 30 ------------------- 5 files changed, 58 deletions(-) diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index adeac8a8adf..af640440959 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -1000,8 +1000,6 @@ function registerTaskListeners(task) { task.on('task:offerConsult', updateTaskList); - task.on('task:offerCampaignReservation', updateTaskList); - task.on('task:consultAccepted', updateTaskList); task.on('task:consulting', updateTaskList); diff --git a/packages/@webex/contact-center/src/services/config/types.ts b/packages/@webex/contact-center/src/services/config/types.ts index 81ee07503a8..8ab9e4c961e 100644 --- a/packages/@webex/contact-center/src/services/config/types.ts +++ b/packages/@webex/contact-center/src/services/config/types.ts @@ -105,8 +105,6 @@ export const CC_TASK_EVENTS = { AGENT_CONTACT: 'AgentContact', /** Event emitted when contact is offered to agent */ AGENT_OFFER_CONTACT: 'AgentOfferContact', - /** Event emitted when campaign reservation is offered to agent */ - AGENT_OFFER_CAMPAIGN_RESERVATION: 'AgentOfferCampaignReservation', /** Event emitted when contact is assigned to agent */ AGENT_CONTACT_ASSIGNED: 'AgentContactAssigned', /** Event emitted when contact is unassigned from agent */ diff --git a/packages/@webex/contact-center/src/services/task/TaskManager.ts b/packages/@webex/contact-center/src/services/task/TaskManager.ts index ad09954865c..48dbd3fe4e2 100644 --- a/packages/@webex/contact-center/src/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/src/services/task/TaskManager.ts @@ -196,18 +196,6 @@ export default class TaskManager extends EventEmitter { this.emit(TASK_EVENTS.TASK_INCOMING, task); } break; - case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: - task = this.updateTaskData(task, payload.data); - LoggerProxy.log(`Agent offer campaign reservation received for task`, { - module: TASK_MANAGER_FILE, - method: METHODS.REGISTER_TASK_LISTENERS, - interactionId: payload.data?.interactionId, - }); - this.emit(TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, task); - - // Handle auto-answer for campaign reservation - this.handleAutoAnswer(task); - break; case CC_EVENTS.AGENT_OFFER_CONTACT: // We don't have to emit any event here since this will be result of promise. task = this.updateTaskData(task, payload.data); diff --git a/packages/@webex/contact-center/src/services/task/types.ts b/packages/@webex/contact-center/src/services/task/types.ts index b293b33a877..4be6a5b3435 100644 --- a/packages/@webex/contact-center/src/services/task/types.ts +++ b/packages/@webex/contact-center/src/services/task/types.ts @@ -371,18 +371,6 @@ export enum TASK_EVENTS { */ TASK_OFFER_CONTACT = 'task:offerContact', - /** - * Triggered when a campaign reservation is offered to agent - * @example - * ```typescript - * task.on(TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, (task: ITask) => { - * console.log('Campaign reservation offered:', task.data.interactionId); - * // Handle campaign reservation offer - * }); - * ``` - */ - TASK_OFFER_CAMPAIGN_RESERVATION = 'task:offerCampaignReservation', - /** * Triggered when a conference is being established * @example diff --git a/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts b/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts index 401a695c655..b1e32e66700 100644 --- a/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/test/unit/spec/services/task/TaskManager.ts @@ -2106,35 +2106,5 @@ describe('TaskManager', () => { }); }); - describe('Auto-Answer Functionality', () => { - // Note: The auto-answer helper functions are thoroughly tested in TaskUtils.ts - - it('should handle AGENT_OFFER_CAMPAIGN_RESERVATION event', () => { - const campaignPayload = { - data: { - ...initalPayload.data, - type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION, - }, - }; - - // Create initial task - webSocketManagerMock.emit('message', JSON.stringify(initalPayload)); - - const managerEmitSpy = jest.spyOn(taskManager, 'emit'); - - // Emit campaign reservation event - webSocketManagerMock.emit('message', JSON.stringify(campaignPayload)); - - // Verify the manager emitted the new TASK_OFFER_CAMPAIGN_RESERVATION event - expect(managerEmitSpy).toHaveBeenCalledWith( - TASK_EVENTS.TASK_OFFER_CAMPAIGN_RESERVATION, - expect.objectContaining({ - data: expect.objectContaining({ - interactionId: taskId - }) - }) - ); - }); - }); }); From 14b86e469ec71b3cef1666e950aa469ae6a20284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Tue, 18 Nov 2025 18:08:09 +0530 Subject: [PATCH 5/5] fix(handle-auto-answer): add-isautoanswering-property-to-diable-accept-decline-button-client-side --- docs/samples/contact-center/app.js | 31 +++++++-- .../src/services/task/TaskManager.ts | 69 +++++++++++-------- .../src/services/task/constants.ts | 2 + .../contact-center/src/services/task/types.ts | 2 + 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index af640440959..ea49291f072 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -2127,6 +2127,7 @@ function renderTaskList(taskList) { const isNew = isIncomingTask(task, agentId); const isTelephony = task.data.interaction.mediaType === 'telephony'; const isBrowserPhone = agentDeviceType === 'BROWSER'; + const isAutoAnswering = task.data.isAutoAnswering || false; // Determine which buttons to show const showAcceptButton = isNew && (isBrowserPhone || !isTelephony); @@ -2136,8 +2137,8 @@ function renderTaskList(taskList) { taskElement.innerHTML = `

${callerDisplay}

- ${showAcceptButton ? `` : ''} - ${showDeclineButton ? `` : ''} + ${showAcceptButton ? `` : ''} + ${showDeclineButton ? `` : ''}

`; @@ -2208,24 +2209,40 @@ function renderTaskList(taskList) { function enableAnswerDeclineButtons(task) { const callerDisplay = task.data.interaction?.callAssociatedDetails?.ani; const isNew = isIncomingTask(task, agentId); - const chatAndSocial = ['chat', 'social']; + const isAutoAnswering = task.data.isAutoAnswering || false; + const chatAndSocial = ['chat', 'social']; + if (task.data.interaction.mediaType === 'telephony') { if (agentDeviceType === 'BROWSER') { - answerElm.disabled = !isNew; - declineElm.disabled = !isNew; + // Disable buttons if auto-answering or not new + answerElm.disabled = !isNew || isAutoAnswering; + declineElm.disabled = !isNew || isAutoAnswering; incomingDetailsElm.innerText = `Call from ${callerDisplay}`; + + // Log auto-answer status for debugging + if (isAutoAnswering) { + console.log('✅ Auto-answer in progress for task:', task.data.interactionId); + } } else { incomingDetailsElm.innerText = `Call from ${callerDisplay}...please answer on the endpoint where the agent's extension is registered`; } } else if (chatAndSocial.includes(task.data.interaction.mediaType)) { - answerElm.disabled = !isNew; + answerElm.disabled = !isNew || isAutoAnswering; declineElm.disabled = true; incomingDetailsElm.innerText = `Chat from ${callerDisplay}`; + + if (isAutoAnswering) { + console.log('✅ Auto-answer in progress for task:', task.data.interactionId); + } } else if (task.data.interaction.mediaType === 'email') { - answerElm.disabled = !isNew; + answerElm.disabled = !isNew || isAutoAnswering; declineElm.disabled = true; incomingDetailsElm.innerText = `Email from ${callerDisplay}`; + + if (isAutoAnswering) { + console.log('✅ Auto-answer in progress for task:', task.data.interactionId); + } } } diff --git a/packages/@webex/contact-center/src/services/task/TaskManager.ts b/packages/@webex/contact-center/src/services/task/TaskManager.ts index 48dbd3fe4e2..060568ec8b9 100644 --- a/packages/@webex/contact-center/src/services/task/TaskManager.ts +++ b/packages/@webex/contact-center/src/services/task/TaskManager.ts @@ -136,6 +136,14 @@ export default class TaskManager extends EventEmitter { interactionId: payload.data.interactionId, }); + // Check if auto-answer should happen for this task + const shouldAutoAnswer = shouldAutoAnswerTask( + payload.data, + this.agentId, + this.webCallingService.loginOption, + this.webRtcEnabled + ); + task = new Task( this.contact, this.webCallingService, @@ -144,6 +152,7 @@ export default class TaskManager extends EventEmitter { wrapUpRequired: payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false, isConferenceInProgress: getIsConferenceInProgress(payload.data), + isAutoAnswering: shouldAutoAnswer, // Set flag before emitting }, this.wrapupData, this.agentId @@ -175,13 +184,22 @@ export default class TaskManager extends EventEmitter { } break; - case CC_EVENTS.AGENT_CONTACT_RESERVED: + case CC_EVENTS.AGENT_CONTACT_RESERVED: { + // Check if auto-answer should happen for this task + const shouldAutoAnswerReserved = shouldAutoAnswerTask( + payload.data, + this.agentId, + this.webCallingService.loginOption, + this.webRtcEnabled + ); + task = new Task( this.contact, this.webCallingService, { ...payload.data, isConsulted: false, + isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting }, this.wrapupData, this.agentId @@ -196,6 +214,7 @@ export default class TaskManager extends EventEmitter { this.emit(TASK_EVENTS.TASK_INCOMING, task); } break; + } case CC_EVENTS.AGENT_OFFER_CONTACT: // We don't have to emit any event here since this will be result of promise. task = this.updateTaskData(task, payload.data); @@ -552,48 +571,42 @@ export default class TaskManager extends EventEmitter { /** * Handles auto-answer logic for incoming tasks - * Automatically accepts tasks when certain conditions are met: + * Automatically accepts tasks when isAutoAnswering flag is set + * The flag is set during task creation based on: * 1. WebRTC calls with auto-answer enabled in agent profile * 2. Agent-initiated WebRTC outdial calls * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers * - * @param task - The task to evaluate for auto-answer + * @param task - The task to auto-answer * @private */ private async handleAutoAnswer(task: ITask): Promise { - if (!task || !task.data) { + if (!task || !task.data || !task.data.isAutoAnswering) { return; } - const shouldAutoAnswer = shouldAutoAnswerTask( - task.data, - this.agentId, - this.webCallingService.loginOption, - this.webRtcEnabled - ); + LoggerProxy.info(`Auto-answering task`, { + module: TASK_MANAGER_FILE, + method: 'handleAutoAnswer', + interactionId: task.data.interactionId, + }); - if (shouldAutoAnswer) { - LoggerProxy.info(`Auto-answering task`, { + try { + await task.accept(); + LoggerProxy.info(`Task auto-answered successfully`, { module: TASK_MANAGER_FILE, method: 'handleAutoAnswer', interactionId: task.data.interactionId, }); - - try { - await task.accept(); - LoggerProxy.info(`Task auto-answered successfully`, { - module: TASK_MANAGER_FILE, - method: 'handleAutoAnswer', - interactionId: task.data.interactionId, - }); - } catch (error) { - LoggerProxy.error(`Failed to auto-answer task`, { - module: TASK_MANAGER_FILE, - method: 'handleAutoAnswer', - interactionId: task.data.interactionId, - error, - }); - } + } catch (error) { + // Reset isAutoAnswering flag on failure + task.updateTaskData({...task.data, isAutoAnswering: false}); + LoggerProxy.error(`Failed to auto-answer task`, { + module: TASK_MANAGER_FILE, + method: 'handleAutoAnswer', + interactionId: task.data.interactionId, + error, + }); } } diff --git a/packages/@webex/contact-center/src/services/task/constants.ts b/packages/@webex/contact-center/src/services/task/constants.ts index 53a79177bba..803cc3fc732 100644 --- a/packages/@webex/contact-center/src/services/task/constants.ts +++ b/packages/@webex/contact-center/src/services/task/constants.ts @@ -34,6 +34,8 @@ export const PRESERVED_TASK_DATA_FIELDS = { WRAP_UP_REQUIRED: 'wrapUpRequired', /** Indicates if a conference is currently in progress (2+ active agents) */ IS_CONFERENCE_IN_PROGRESS: 'isConferenceInProgress', + /** Indicates if auto-answer is in progress for this task */ + IS_AUTO_ANSWERING: 'isAutoAnswering', }; /** diff --git a/packages/@webex/contact-center/src/services/task/types.ts b/packages/@webex/contact-center/src/services/task/types.ts index 4be6a5b3435..56030e3d756 100644 --- a/packages/@webex/contact-center/src/services/task/types.ts +++ b/packages/@webex/contact-center/src/services/task/types.ts @@ -787,6 +787,8 @@ export type TaskData = { reservedAgentChannelId?: string; /** Indicates if wrap-up is required for this task */ wrapUpRequired?: boolean; + /** Indicates if auto-answer is in progress for this task */ + isAutoAnswering?: boolean; }; /**