@webex/contact-center 3.9.0-next.2 → 3.9.0-next.4
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/core/Utils.js +75 -1
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/task/TaskManager.js +1 -0
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/index.js +23 -19
- package/dist/services/task/index.js.map +1 -1
- package/dist/types/services/core/Utils.d.ts +11 -0
- package/dist/types/services/task/index.d.ts +1 -1
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/core/Utils.ts +98 -0
- package/src/services/task/TaskManager.ts +1 -0
- package/src/services/task/index.ts +49 -29
- package/test/unit/spec/services/core/Utils.ts +50 -0
- package/test/unit/spec/services/task/TaskManager.ts +8 -1
- package/test/unit/spec/services/task/index.ts +73 -11
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
import {CALL_EVENT_KEYS, LocalMicrophoneStream} from '@webex/calling';
|
|
3
3
|
import {CallId} from '@webex/calling/dist/types/common/types';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getErrorDetails,
|
|
6
|
+
deriveConsultTransferDestinationType,
|
|
7
|
+
getDestinationAgentId,
|
|
8
|
+
} from '../core/Utils';
|
|
5
9
|
import {LoginOption} from '../../types';
|
|
6
10
|
import {TASK_FILE} from '../../constants';
|
|
7
11
|
import {METHODS} from './constants';
|
|
@@ -19,7 +23,6 @@ import {
|
|
|
19
23
|
ConsultEndPayload,
|
|
20
24
|
TransferPayLoad,
|
|
21
25
|
DESTINATION_TYPE,
|
|
22
|
-
CONSULT_TRANSFER_DESTINATION_TYPE,
|
|
23
26
|
ConsultTransferPayLoad,
|
|
24
27
|
MEDIA_CHANNEL,
|
|
25
28
|
} from './types';
|
|
@@ -1308,61 +1311,78 @@ export default class Task extends EventEmitter implements ITask {
|
|
|
1308
1311
|
* ```
|
|
1309
1312
|
*/
|
|
1310
1313
|
public async consultTransfer(
|
|
1311
|
-
consultTransferPayload
|
|
1314
|
+
consultTransferPayload?: ConsultTransferPayLoad
|
|
1312
1315
|
): Promise<TaskResponse> {
|
|
1313
1316
|
try {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1317
|
+
// Get the destination agent ID using custom logic from participants data
|
|
1318
|
+
const destAgentId = getDestinationAgentId(
|
|
1319
|
+
this.data.interaction?.participants,
|
|
1320
|
+
this.data.agentId
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
// Resolve the target id (queue consult transfers go to the accepted agent)
|
|
1324
|
+
if (!destAgentId) {
|
|
1325
|
+
throw new Error('No agent has accepted this queue consult yet');
|
|
1326
|
+
}
|
|
1319
1327
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1328
|
+
LoggerProxy.info(
|
|
1329
|
+
`Initiating consult transfer to ${consultTransferPayload?.to || destAgentId}`,
|
|
1330
|
+
{
|
|
1331
|
+
module: TASK_FILE,
|
|
1332
|
+
method: METHODS.CONSULT_TRANSFER,
|
|
1333
|
+
interactionId: this.data.interactionId,
|
|
1324
1334
|
}
|
|
1335
|
+
);
|
|
1336
|
+
// Obtain payload based on desktop logic using TaskData
|
|
1337
|
+
const finalDestinationType = deriveConsultTransferDestinationType(this.data);
|
|
1325
1338
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1339
|
+
// By default we always use the computed destAgentId as the target id
|
|
1340
|
+
const consultTransferRequest: ConsultTransferPayLoad = {
|
|
1341
|
+
to: destAgentId,
|
|
1342
|
+
destinationType: finalDestinationType,
|
|
1343
|
+
};
|
|
1332
1344
|
|
|
1333
1345
|
const result = await this.contact.consultTransfer({
|
|
1334
1346
|
interactionId: this.data.interactionId,
|
|
1335
|
-
data:
|
|
1347
|
+
data: consultTransferRequest,
|
|
1336
1348
|
});
|
|
1337
1349
|
|
|
1338
1350
|
this.metricsManager.trackEvent(
|
|
1339
1351
|
METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
|
|
1340
1352
|
{
|
|
1341
1353
|
taskId: this.data.interactionId,
|
|
1342
|
-
destination:
|
|
1343
|
-
destinationType:
|
|
1354
|
+
destination: consultTransferRequest.to,
|
|
1355
|
+
destinationType: consultTransferRequest.destinationType,
|
|
1344
1356
|
isConsultTransfer: true,
|
|
1345
1357
|
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1346
1358
|
},
|
|
1347
1359
|
['operational', 'behavioral', 'business']
|
|
1348
1360
|
);
|
|
1349
1361
|
|
|
1350
|
-
LoggerProxy.log(
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1362
|
+
LoggerProxy.log(
|
|
1363
|
+
`Consult transfer completed successfully to ${consultTransferPayload?.to || destAgentId}`,
|
|
1364
|
+
{
|
|
1365
|
+
module: TASK_FILE,
|
|
1366
|
+
method: METHODS.CONSULT_TRANSFER,
|
|
1367
|
+
trackingId: result.trackingId,
|
|
1368
|
+
interactionId: this.data.interactionId,
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1356
1371
|
|
|
1357
1372
|
return result;
|
|
1358
1373
|
} catch (error) {
|
|
1359
1374
|
const {error: detailedError} = getErrorDetails(error, METHODS.CONSULT_TRANSFER, TASK_FILE);
|
|
1375
|
+
const failedDestinationType = deriveConsultTransferDestinationType(this.data);
|
|
1376
|
+
const failedDestAgentId = getDestinationAgentId(
|
|
1377
|
+
this.data.interaction?.participants,
|
|
1378
|
+
this.data.agentId
|
|
1379
|
+
);
|
|
1360
1380
|
this.metricsManager.trackEvent(
|
|
1361
1381
|
METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
|
|
1362
1382
|
{
|
|
1363
1383
|
taskId: this.data.interactionId,
|
|
1364
|
-
destination:
|
|
1365
|
-
destinationType:
|
|
1384
|
+
destination: failedDestAgentId || '',
|
|
1385
|
+
destinationType: failedDestinationType,
|
|
1366
1386
|
isConsultTransfer: true,
|
|
1367
1387
|
error: error.toString(),
|
|
1368
1388
|
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
|
|
@@ -276,4 +276,54 @@ describe('Utils', () => {
|
|
|
276
276
|
});
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
describe('getDestinationAgentId', () => {
|
|
281
|
+
const currentAgentId = 'agent-current-123';
|
|
282
|
+
|
|
283
|
+
it('returns another Agent id when present and not in wrap-up', () => {
|
|
284
|
+
const participants: any = {
|
|
285
|
+
[currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
|
|
286
|
+
agent1: {type: 'Agent', id: 'agent-1', isWrapUp: false},
|
|
287
|
+
customer1: {type: 'Customer', id: 'cust-1', isWrapUp: false},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = Utils.getDestinationAgentId(participants, currentAgentId);
|
|
291
|
+
expect(result).toBe('agent-1');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('ignores self and wrap-up participants', () => {
|
|
295
|
+
const participants: any = {
|
|
296
|
+
[currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
|
|
297
|
+
agentWrap: {type: 'Agent', id: 'agent-wrap', isWrapUp: true},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const result = Utils.getDestinationAgentId(participants, currentAgentId);
|
|
301
|
+
expect(result).toBe('');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('supports DN, EpDn and entryPoint types', () => {
|
|
305
|
+
const participantsDN: any = {
|
|
306
|
+
[currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
|
|
307
|
+
dn1: {type: 'DN', id: 'dn-1', isWrapUp: false},
|
|
308
|
+
};
|
|
309
|
+
expect(Utils.getDestinationAgentId(participantsDN, currentAgentId)).toBe('dn-1');
|
|
310
|
+
|
|
311
|
+
const participantsEpDn: any = {
|
|
312
|
+
[currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
|
|
313
|
+
epdn1: {type: 'EpDn', id: 'epdn-1', isWrapUp: false},
|
|
314
|
+
};
|
|
315
|
+
expect(Utils.getDestinationAgentId(participantsEpDn, currentAgentId)).toBe('epdn-1');
|
|
316
|
+
|
|
317
|
+
const participantsEntry: any = {
|
|
318
|
+
[currentAgentId]: {type: 'Agent', id: currentAgentId, isWrapUp: false},
|
|
319
|
+
entry1: {type: 'entryPoint', id: 'entry-1', isWrapUp: false},
|
|
320
|
+
};
|
|
321
|
+
expect(Utils.getDestinationAgentId(participantsEntry, currentAgentId)).toBe('entry-1');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns empty string when participants is missing or empty', () => {
|
|
325
|
+
expect(Utils.getDestinationAgentId(undefined as any, currentAgentId)).toBe('');
|
|
326
|
+
expect(Utils.getDestinationAgentId({} as any, currentAgentId)).toBe('');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
279
329
|
});
|
|
@@ -658,6 +658,13 @@ describe('TaskManager', () => {
|
|
|
658
658
|
});
|
|
659
659
|
|
|
660
660
|
it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
|
|
661
|
+
const initialConsultingPayload = {
|
|
662
|
+
data: {
|
|
663
|
+
...initalPayload.data,
|
|
664
|
+
type: CC_EVENTS.AGENT_OFFER_CONSULT,
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
|
|
661
668
|
const consultingPayload = {
|
|
662
669
|
data: {
|
|
663
670
|
...initalPayload.data,
|
|
@@ -672,8 +679,8 @@ describe('TaskManager', () => {
|
|
|
672
679
|
});
|
|
673
680
|
|
|
674
681
|
const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
|
|
682
|
+
webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
|
|
675
683
|
webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
|
|
676
|
-
expect(taskManager.getTask(taskId).updateTaskData).toHaveBeenCalledWith(consultingPayload.data);
|
|
677
684
|
expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
|
|
678
685
|
expect(taskEmitSpy).toHaveBeenCalledWith(
|
|
679
686
|
TASK_EVENTS.TASK_CONSULT_ACCEPTED,
|
|
@@ -39,6 +39,7 @@ describe('Task', () => {
|
|
|
39
39
|
let loggerInfoSpy;
|
|
40
40
|
let loggerLogSpy;
|
|
41
41
|
let loggerErrorSpy;
|
|
42
|
+
let getDestinationAgentIdSpy;
|
|
42
43
|
|
|
43
44
|
const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
|
|
44
45
|
const mockTrack = {} as MediaStreamTrack;
|
|
@@ -141,6 +142,11 @@ describe('Task', () => {
|
|
|
141
142
|
},
|
|
142
143
|
};
|
|
143
144
|
|
|
145
|
+
// Mock destination agent id resolution from participants
|
|
146
|
+
getDestinationAgentIdSpy = jest
|
|
147
|
+
.spyOn(Utils, 'getDestinationAgentId')
|
|
148
|
+
.mockReturnValue(taskDataMock.destAgentId);
|
|
149
|
+
|
|
144
150
|
// Create an instance of Task
|
|
145
151
|
task = new Task(contactMock, webCallingService, taskDataMock);
|
|
146
152
|
|
|
@@ -163,6 +169,7 @@ describe('Task', () => {
|
|
|
163
169
|
|
|
164
170
|
afterEach(() => {
|
|
165
171
|
jest.clearAllMocks();
|
|
172
|
+
jest.restoreAllMocks();
|
|
166
173
|
});
|
|
167
174
|
|
|
168
175
|
it('test the on spy', async () => {
|
|
@@ -754,29 +761,81 @@ describe('Task', () => {
|
|
|
754
761
|
expect(contactMock.consult).toHaveBeenCalledWith({interactionId: taskId, data: consultPayload});
|
|
755
762
|
expect(response).toEqual(expectedResponse);
|
|
756
763
|
|
|
757
|
-
const
|
|
758
|
-
to: '1234',
|
|
759
|
-
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const consultTransferResponse = await task.consultTransfer(consultTransferPayload);
|
|
764
|
+
const consultTransferResponse = await task.consultTransfer();
|
|
763
765
|
expect(contactMock.consultTransfer).toHaveBeenCalledWith({
|
|
764
766
|
interactionId: taskId,
|
|
765
|
-
data:
|
|
767
|
+
data: {
|
|
768
|
+
to: taskDataMock.destAgentId,
|
|
769
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
770
|
+
},
|
|
766
771
|
});
|
|
767
772
|
expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
|
|
768
773
|
2,
|
|
769
774
|
METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
|
|
770
775
|
{
|
|
771
776
|
taskId: taskDataMock.interactionId,
|
|
772
|
-
destination:
|
|
773
|
-
destinationType:
|
|
777
|
+
destination: taskDataMock.destAgentId,
|
|
778
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
774
779
|
isConsultTransfer: true,
|
|
775
780
|
},
|
|
776
781
|
['operational', 'behavioral', 'business']
|
|
777
782
|
);
|
|
778
783
|
});
|
|
779
784
|
|
|
785
|
+
it('should send DIALNUMBER when task destinationType is DN during consultTransfer', async () => {
|
|
786
|
+
const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
|
|
787
|
+
contactMock.consultTransfer.mockResolvedValue(expectedResponse);
|
|
788
|
+
|
|
789
|
+
// Ensure task data indicates DN scenario
|
|
790
|
+
task.data.destinationType = 'DN' as unknown as string;
|
|
791
|
+
|
|
792
|
+
await task.consultTransfer();
|
|
793
|
+
|
|
794
|
+
expect(contactMock.consultTransfer).toHaveBeenCalledWith({
|
|
795
|
+
interactionId: taskId,
|
|
796
|
+
data: {
|
|
797
|
+
to: taskDataMock.destAgentId,
|
|
798
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER,
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should send ENTRYPOINT when task destinationType is EPDN during consultTransfer', async () => {
|
|
804
|
+
const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
|
|
805
|
+
contactMock.consultTransfer.mockResolvedValue(expectedResponse);
|
|
806
|
+
|
|
807
|
+
// Ensure task data indicates EP/EPDN scenario
|
|
808
|
+
task.data.destinationType = 'EPDN' as unknown as string;
|
|
809
|
+
|
|
810
|
+
await task.consultTransfer();
|
|
811
|
+
|
|
812
|
+
expect(contactMock.consultTransfer).toHaveBeenCalledWith({
|
|
813
|
+
interactionId: taskId,
|
|
814
|
+
data: {
|
|
815
|
+
to: taskDataMock.destAgentId,
|
|
816
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT,
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should keep AGENT when task destinationType is neither DN nor EPDN/ENTRYPOINT', async () => {
|
|
822
|
+
const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
|
|
823
|
+
contactMock.consultTransfer.mockResolvedValue(expectedResponse);
|
|
824
|
+
|
|
825
|
+
// Ensure task data indicates non-DN and non-EP/EPDN scenario
|
|
826
|
+
task.data.destinationType = 'SOMETHING_ELSE' as unknown as string;
|
|
827
|
+
|
|
828
|
+
await task.consultTransfer();
|
|
829
|
+
|
|
830
|
+
expect(contactMock.consultTransfer).toHaveBeenCalledWith({
|
|
831
|
+
interactionId: taskId,
|
|
832
|
+
data: {
|
|
833
|
+
to: taskDataMock.destAgentId,
|
|
834
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
780
839
|
it('should do consult transfer to a queue by using the destAgentId from task data', async () => {
|
|
781
840
|
const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
|
|
782
841
|
contactMock.consultTransfer.mockResolvedValue(expectedResponse);
|
|
@@ -811,6 +870,9 @@ describe('Task', () => {
|
|
|
811
870
|
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE,
|
|
812
871
|
};
|
|
813
872
|
|
|
873
|
+
// For this negative case, ensure computed destination is empty
|
|
874
|
+
getDestinationAgentIdSpy.mockReturnValueOnce('');
|
|
875
|
+
|
|
814
876
|
await expect(
|
|
815
877
|
taskWithoutDestAgentId.consultTransfer(queueConsultTransferPayload)
|
|
816
878
|
).rejects.toThrow('Error while performing consultTransfer');
|
|
@@ -855,8 +917,8 @@ describe('Task', () => {
|
|
|
855
917
|
METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
|
|
856
918
|
{
|
|
857
919
|
taskId: taskDataMock.interactionId,
|
|
858
|
-
destination:
|
|
859
|
-
destinationType:
|
|
920
|
+
destination: taskDataMock.destAgentId,
|
|
921
|
+
destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
|
|
860
922
|
isConsultTransfer: true,
|
|
861
923
|
error: error.toString(),
|
|
862
924
|
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details),
|