Skip to content

Commit ebeba5f

Browse files
authored
feat(MPC): add-is-conference-in-progress-to-agent-contact-event (#4558)
1 parent c834970 commit ebeba5f

File tree

4 files changed

+243
-9
lines changed

4 files changed

+243
-9
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,20 @@ export default class TaskManager extends EventEmitter {
128128
method: METHODS.REGISTER_TASK_LISTENERS,
129129
interactionId: payload.data.interactionId,
130130
});
131+
132+
// Pre-calculate isConferenceInProgress for the initial task data
133+
const simulatedTaskForAgentContact = {
134+
data: {...payload.data},
135+
} as ITask;
136+
131137
task = new Task(
132138
this.contact,
133139
this.webCallingService,
134140
{
135141
...payload.data,
136142
wrapUpRequired:
137143
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
144+
isConferenceInProgress: getIsConferenceInProgress(simulatedTaskForAgentContact),
138145
},
139146
this.wrapupData,
140147
this.agentId
@@ -165,6 +172,7 @@ export default class TaskManager extends EventEmitter {
165172
}
166173
}
167174
break;
175+
168176
case CC_EVENTS.AGENT_CONTACT_RESERVED:
169177
task = new Task(
170178
this.contact,

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,14 +1556,24 @@ export default class Task extends EventEmitter implements ITask {
15561556
* ```
15571557
*/
15581558
public async consultConference(): Promise<TaskResponse> {
1559-
// Extract consultation conference data from task data (used in both try and catch)
1560-
const consultationData = {
1561-
agentId: this.agentId,
1562-
destAgentId: this.data.destAgentId,
1563-
destinationType: this.data.destinationType || 'agent',
1564-
};
1565-
15661559
try {
1560+
// Get the destination agent ID using custom logic from participants data (same as consultTransfer)
1561+
const destAgentId = getDestinationAgentId(
1562+
this.data.interaction?.participants,
1563+
this.data.agentId
1564+
);
1565+
1566+
// Validate that we have a destination agent (for queue consult scenarios)
1567+
if (!destAgentId) {
1568+
throw new Error('No agent has accepted this queue consult yet');
1569+
}
1570+
// Extract consultation conference data from task data (used in both try and catch)
1571+
const consultationData = {
1572+
agentId: this.agentId,
1573+
destAgentId,
1574+
destinationType: this.data.destinationType || 'agent',
1575+
};
1576+
15671577
LoggerProxy.info(`Initiating consult conference to ${consultationData.destAgentId}`, {
15681578
module: TASK_FILE,
15691579
method: METHODS.CONSULT_CONFERENCE,
@@ -1611,9 +1621,21 @@ export default class Task extends EventEmitter implements ITask {
16111621
};
16121622

16131623
// Track failure metrics (following consultTransfer pattern)
1614-
// Build conference data for error tracking using extracted data
1624+
// Recalculate destination info for error tracking
1625+
const failedDestAgentId = getDestinationAgentId(
1626+
this.data.interaction?.participants,
1627+
this.data.agentId
1628+
);
1629+
1630+
// Build conference data for error tracking using recalculated data
1631+
const failedConsultationData = {
1632+
agentId: this.agentId,
1633+
destAgentId: failedDestAgentId,
1634+
destinationType: this.data.destinationType || 'agent',
1635+
};
1636+
16151637
const failedParamsData = buildConsultConferenceParamData(
1616-
consultationData,
1638+
failedConsultationData,
16171639
this.data.interactionId
16181640
);
16191641

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

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,73 @@ describe('TaskManager', () => {
479479
);
480480
});
481481

482+
it('should set isConferenceInProgress correctly when creating task via AGENT_CONTACT with conference in progress', () => {
483+
const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
484+
taskManager.setAgentId(testAgentId);
485+
taskManager.taskCollection = [];
486+
487+
const payload = {
488+
data: {
489+
...initalPayload.data,
490+
type: CC_EVENTS.AGENT_CONTACT,
491+
interaction: {
492+
mediaType: 'telephony',
493+
state: 'conference',
494+
participants: {
495+
[testAgentId]: { pType: 'Agent', hasLeft: false },
496+
'agent-2': { pType: 'Agent', hasLeft: false },
497+
'customer-1': { pType: 'Customer', hasLeft: false },
498+
},
499+
media: {
500+
[taskId]: {
501+
mType: 'mainCall',
502+
participants: [testAgentId, 'agent-2', 'customer-1'],
503+
},
504+
},
505+
},
506+
},
507+
};
508+
509+
webSocketManagerMock.emit('message', JSON.stringify(payload));
510+
511+
const createdTask = taskManager.getTask(taskId);
512+
expect(createdTask).toBeDefined();
513+
expect(createdTask.data.isConferenceInProgress).toBe(true);
514+
});
515+
516+
it('should set isConferenceInProgress to false when creating task via AGENT_CONTACT with only one agent', () => {
517+
const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
518+
taskManager.setAgentId(testAgentId);
519+
taskManager.taskCollection = [];
520+
521+
const payload = {
522+
data: {
523+
...initalPayload.data,
524+
type: CC_EVENTS.AGENT_CONTACT,
525+
interaction: {
526+
mediaType: 'telephony',
527+
state: 'connected',
528+
participants: {
529+
[testAgentId]: { pType: 'Agent', hasLeft: false },
530+
'customer-1': { pType: 'Customer', hasLeft: false },
531+
},
532+
media: {
533+
[taskId]: {
534+
mType: 'mainCall',
535+
participants: [testAgentId, 'customer-1'],
536+
},
537+
},
538+
},
539+
},
540+
};
541+
542+
webSocketManagerMock.emit('message', JSON.stringify(payload));
543+
544+
const createdTask = taskManager.getTask(taskId);
545+
expect(createdTask).toBeDefined();
546+
expect(createdTask.data.isConferenceInProgress).toBe(false);
547+
});
548+
482549
it('should emit TASK_END event on AGENT_WRAPUP event', () => {
483550
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
484551

@@ -1438,7 +1505,86 @@ describe('TaskManager', () => {
14381505
// No specific task event emission for participant joined - just data update
14391506
});
14401507

1508+
it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1509+
const payload = {
1510+
data: {
1511+
type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1512+
interactionId: taskId,
1513+
participantId: 'new-agent-789',
1514+
interaction: {
1515+
participants: {
1516+
[agentId]: { pType: 'Agent', hasLeft: false },
1517+
'agent-2': { pType: 'Agent', hasLeft: false },
1518+
'new-agent-789': { pType: 'Agent', hasLeft: false },
1519+
'customer-1': { pType: 'Customer', hasLeft: false },
1520+
},
1521+
media: {
1522+
[taskId]: {
1523+
mType: 'mainCall',
1524+
participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
1525+
},
1526+
},
1527+
},
1528+
},
1529+
};
1530+
1531+
const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1532+
1533+
webSocketManagerMock.emit('message', JSON.stringify(payload));
1534+
1535+
// Verify updateTaskData was called exactly once
1536+
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1537+
1538+
// Verify it was called with isConferenceInProgress already calculated
1539+
expect(updateTaskDataSpy).toHaveBeenCalledWith(
1540+
expect.objectContaining({
1541+
participantId: 'new-agent-789',
1542+
isConferenceInProgress: true, // 3 active agents
1543+
})
1544+
);
1545+
1546+
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
1547+
});
1548+
14411549
describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
1550+
it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1551+
const payload = {
1552+
data: {
1553+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1554+
interactionId: taskId,
1555+
interaction: {
1556+
participants: {
1557+
[agentId]: { pType: 'Agent', hasLeft: false },
1558+
'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
1559+
'customer-1': { pType: 'Customer', hasLeft: false },
1560+
},
1561+
media: {
1562+
[taskId]: {
1563+
mType: 'mainCall',
1564+
participants: [agentId, 'customer-1'], // agent-2 removed from participants
1565+
},
1566+
},
1567+
},
1568+
},
1569+
};
1570+
1571+
const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1572+
1573+
webSocketManagerMock.emit('message', JSON.stringify(payload));
1574+
1575+
// Verify updateTaskData was called exactly once
1576+
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1577+
1578+
// Verify it was called with isConferenceInProgress already calculated
1579+
expect(updateTaskDataSpy).toHaveBeenCalledWith(
1580+
expect.objectContaining({
1581+
isConferenceInProgress: false, // Only 1 active agent remains
1582+
})
1583+
);
1584+
1585+
expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1586+
});
1587+
14421588
it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
14431589
const payload = {
14441590
data: {

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,64 @@ describe('Task', () => {
18301830
interactionId: taskId,
18311831
});
18321832
});
1833+
1834+
it('should dynamically calculate destAgentId from participants when this.data.destAgentId is null', async () => {
1835+
// Simulate scenario where destAgentId is not preserved (e.g., after hold/unhold)
1836+
task.data.destAgentId = null;
1837+
1838+
const consultedAgentId = 'consulted-agent-123';
1839+
getDestinationAgentIdSpy.mockReturnValue(consultedAgentId);
1840+
1841+
const mockResponse = {
1842+
trackingId: 'test-tracking-dynamic',
1843+
interactionId: taskId,
1844+
};
1845+
contactMock.consultConference.mockResolvedValue(mockResponse);
1846+
1847+
const result = await task.consultConference();
1848+
1849+
// Verify getDestinationAgentId was called to dynamically calculate the destination
1850+
expect(getDestinationAgentIdSpy).toHaveBeenCalledWith(
1851+
taskDataMock.interaction?.participants,
1852+
taskDataMock.agentId
1853+
);
1854+
1855+
// Verify the conference was called with the dynamically calculated destAgentId
1856+
expect(contactMock.consultConference).toHaveBeenCalledWith({
1857+
interactionId: taskId,
1858+
data: {
1859+
agentId: taskDataMock.agentId,
1860+
to: consultedAgentId, // Dynamically calculated value
1861+
destinationType: 'agent',
1862+
},
1863+
});
1864+
expect(result).toEqual(mockResponse);
1865+
});
1866+
1867+
it('should throw error when no destination agent is found (queue consult not accepted)', async () => {
1868+
// Simulate queue consult scenario where no agent has accepted yet
1869+
getDestinationAgentIdSpy.mockReturnValue(''); // No agent found
1870+
1871+
// Mock generateTaskErrorObject to wrap the error
1872+
const wrappedError = new Error('Error while performing consultConference');
1873+
generateTaskErrorObjectSpy.mockReturnValue(wrappedError);
1874+
1875+
await expect(task.consultConference()).rejects.toThrow('Error while performing consultConference');
1876+
1877+
// Verify the conference was NOT called
1878+
expect(contactMock.consultConference).not.toHaveBeenCalled();
1879+
1880+
// Verify metrics were tracked for the failure
1881+
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1882+
'Task Conference Start Failed',
1883+
expect.objectContaining({
1884+
taskId: taskId,
1885+
destination: '', // No destination found
1886+
destinationType: 'agent',
1887+
}),
1888+
['operational', 'behavioral', 'business']
1889+
);
1890+
});
18331891
});
18341892

18351893
describe('exitConference', () => {

0 commit comments

Comments
 (0)