@webex/contact-center 3.12.0-task-refactor.10 → 3.12.0-task-refactor.12
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/cc.js +345 -82
- package/dist/cc.js.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -1
- package/dist/metrics/behavioral-events.js +39 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +8 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +3 -0
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +1 -1
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/types.js +10 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +1 -0
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +57 -6
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +92 -17
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +24 -3
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +16 -4
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskManager.js +158 -6
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +39 -11
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +7 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +129 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +30 -3
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +4 -0
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/constants.js +3 -0
- package/dist/services/task/state-machine/constants.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +23 -0
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/types.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/types.js +69 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/types/cc.d.ts +93 -2
- package/dist/types/config.d.ts +6 -0
- package/dist/types/constants.d.ts +3 -0
- package/dist/types/metrics/constants.d.ts +6 -0
- package/dist/types/services/config/types.d.ts +22 -1
- package/dist/types/services/core/Err.d.ts +6 -0
- package/dist/types/services/core/Utils.d.ts +20 -2
- package/dist/types/services/core/aqm-reqs.d.ts +49 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- package/dist/types/services/core/websocket/connection-service.d.ts +1 -0
- package/dist/types/services/task/Task.d.ts +1 -0
- package/dist/types/services/task/TaskUtils.d.ts +13 -0
- package/dist/types/services/task/constants.d.ts +6 -0
- package/dist/types/services/task/dialer.d.ts +45 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +36 -8
- package/dist/types/services/task/state-machine/constants.d.ts +3 -0
- package/dist/types/services/task/state-machine/guards.d.ts +2 -0
- package/dist/types/services/task/state-machine/types.d.ts +11 -0
- package/dist/types/services/task/types.d.ts +85 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/cc.ts +452 -102
- package/src/config.ts +6 -0
- package/src/constants.ts +3 -0
- package/src/metrics/behavioral-events.ts +42 -0
- package/src/metrics/constants.ts +9 -1
- package/src/services/ApiAiAssistant.ts +3 -0
- package/src/services/config/Util.ts +1 -1
- package/src/services/config/types.ts +12 -1
- package/src/services/constants.ts +1 -0
- package/src/services/core/Err.ts +3 -0
- package/src/services/core/Utils.ts +68 -5
- package/src/services/core/aqm-reqs.ts +100 -22
- package/src/services/core/websocket/WebSocketManager.ts +23 -2
- package/src/services/index.ts +1 -0
- package/src/services/task/Task.ts +22 -4
- package/src/services/task/TaskManager.ts +175 -7
- package/src/services/task/TaskUtils.ts +33 -1
- package/src/services/task/constants.ts +6 -0
- package/src/services/task/dialer.ts +136 -1
- package/src/services/task/state-machine/TaskStateMachine.ts +37 -5
- package/src/services/task/state-machine/actions.ts +4 -0
- package/src/services/task/state-machine/constants.ts +3 -0
- package/src/services/task/state-machine/guards.ts +30 -0
- package/src/services/task/state-machine/types.ts +14 -1
- package/src/services/task/state-machine/uiControlsComputer.ts +1 -1
- package/src/services/task/types.ts +91 -0
- package/src/types.ts +6 -1
- package/test/unit/spec/cc.ts +432 -26
- package/test/unit/spec/services/config/index.ts +30 -2
- package/test/unit/spec/services/core/Utils.ts +367 -1
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +90 -1
- package/test/unit/spec/services/core/websocket/connection-service.ts +4 -5
- package/test/unit/spec/services/task/TaskManager.ts +2334 -145
- package/test/unit/spec/services/task/dialer.ts +372 -96
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +47 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
package/src/cc.ts
CHANGED
|
@@ -56,7 +56,13 @@ import {
|
|
|
56
56
|
import {ConnectionLostDetails} from './services/core/websocket/types';
|
|
57
57
|
import TaskManager from './services/task/TaskManager';
|
|
58
58
|
import WebCallingService from './services/WebCallingService';
|
|
59
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
ITask,
|
|
61
|
+
TASK_EVENTS,
|
|
62
|
+
TaskResponse,
|
|
63
|
+
DialerPayload,
|
|
64
|
+
PreviewContactPayload,
|
|
65
|
+
} from './services/task/types';
|
|
60
66
|
import MetricsManager from './metrics/MetricsManager';
|
|
61
67
|
import {METRIC_EVENT_NAMES} from './metrics/constants';
|
|
62
68
|
import {Failure} from './services/core/GlobalTypes';
|
|
@@ -121,6 +127,7 @@ import type {
|
|
|
121
127
|
* - `task:established` - Task/call has been connected
|
|
122
128
|
* - `task:ended` - Task/call has ended
|
|
123
129
|
* - `task:error` - An error occurred during task handling
|
|
130
|
+
* - `task:campaignPreviewReservation` - Campaign preview contact offered to agent
|
|
124
131
|
*
|
|
125
132
|
* @public
|
|
126
133
|
*
|
|
@@ -351,6 +358,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
351
358
|
this.$webex.once(READY, () => {
|
|
352
359
|
// @ts-ignore
|
|
353
360
|
this.$config = this.config;
|
|
361
|
+
this.validatePluginConfig();
|
|
354
362
|
|
|
355
363
|
/**
|
|
356
364
|
* This is used for handling the async requests by sending webex.request and wait for corresponding websocket event.
|
|
@@ -363,7 +371,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
363
371
|
webex: this.$webex,
|
|
364
372
|
connectionConfig: this.getConnectionConfig(),
|
|
365
373
|
});
|
|
366
|
-
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
367
374
|
|
|
368
375
|
this.webCallingService = new WebCallingService(this.$webex);
|
|
369
376
|
this.apiAIAssistant = new ApiAIAssistant(this.$webex);
|
|
@@ -406,6 +413,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
406
413
|
this.trigger(TASK_EVENTS.TASK_HYDRATE, task);
|
|
407
414
|
};
|
|
408
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Handles multi-login hydrate events for SDK instances without Mobius registration
|
|
418
|
+
* @private
|
|
419
|
+
* @param {ITask} task The task object associated with the multi-login hydrate
|
|
420
|
+
*/
|
|
421
|
+
private handleTaskMultiLoginHydrate = (task: ITask) => {
|
|
422
|
+
// @ts-ignore
|
|
423
|
+
this.trigger(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
|
|
424
|
+
};
|
|
425
|
+
|
|
409
426
|
/**
|
|
410
427
|
* Handles task merged events when tasks are combined eg: EPDN merge/transfer
|
|
411
428
|
* @private
|
|
@@ -416,6 +433,20 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
416
433
|
this.trigger(TASK_EVENTS.TASK_MERGED, task);
|
|
417
434
|
};
|
|
418
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Handles campaign preview reservation events when a contact is offered to the agent
|
|
438
|
+
* @private
|
|
439
|
+
* @param {ITask} task The campaign reservation task
|
|
440
|
+
*/
|
|
441
|
+
private handleCampaignPreviewReservation = (task: ITask) => {
|
|
442
|
+
// @ts-ignore
|
|
443
|
+
this.trigger(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
private handleRTDWebsocketMessage = (event: string) => {
|
|
447
|
+
this.taskManager.handleRealtimeWebsocketEvent(event);
|
|
448
|
+
};
|
|
449
|
+
|
|
419
450
|
/**
|
|
420
451
|
* Sets up event listeners for incoming tasks and task hydration
|
|
421
452
|
* Subscribes to task events from the task manager
|
|
@@ -424,7 +455,12 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
424
455
|
private incomingTaskListener() {
|
|
425
456
|
this.taskManager.on(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
|
|
426
457
|
this.taskManager.on(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
|
|
458
|
+
this.taskManager.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this.handleTaskMultiLoginHydrate);
|
|
427
459
|
this.taskManager.on(TASK_EVENTS.TASK_MERGED, this.handleTaskMerged);
|
|
460
|
+
this.taskManager.on(
|
|
461
|
+
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
|
|
462
|
+
this.handleCampaignPreviewReservation
|
|
463
|
+
);
|
|
428
464
|
}
|
|
429
465
|
|
|
430
466
|
/**
|
|
@@ -464,7 +500,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
464
500
|
* ```
|
|
465
501
|
*/
|
|
466
502
|
public async register(): Promise<Profile> {
|
|
467
|
-
LoggerProxy.
|
|
503
|
+
LoggerProxy.log('Starting CC SDK registration', {
|
|
468
504
|
module: CC_FILE,
|
|
469
505
|
method: METHODS.REGISTER,
|
|
470
506
|
});
|
|
@@ -474,6 +510,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
474
510
|
METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_FAILED,
|
|
475
511
|
]);
|
|
476
512
|
this.setupEventListeners();
|
|
513
|
+
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
477
514
|
|
|
478
515
|
const resp = await this.connectWebsocket();
|
|
479
516
|
// Ensure 'dn' is always populated from 'defaultDn'
|
|
@@ -553,6 +590,11 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
553
590
|
|
|
554
591
|
this.taskManager.off(TASK_EVENTS.TASK_INCOMING, this.handleIncomingTask);
|
|
555
592
|
this.taskManager.off(TASK_EVENTS.TASK_HYDRATE, this.handleTaskHydrate);
|
|
593
|
+
this.taskManager.off(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, this.handleTaskMultiLoginHydrate);
|
|
594
|
+
this.taskManager.off(
|
|
595
|
+
TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
|
|
596
|
+
this.handleCampaignPreviewReservation
|
|
597
|
+
);
|
|
556
598
|
this.taskManager.unregisterIncomingCallEvent();
|
|
557
599
|
|
|
558
600
|
this.services.webSocketManager.off('message', this.handleWebsocketMessage);
|
|
@@ -583,6 +625,10 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
583
625
|
this.services.rtdWebSocketManager.close(false, 'Unregistering the SDK');
|
|
584
626
|
}
|
|
585
627
|
|
|
628
|
+
if (this.services.rtdWebSocketManager && !this.services.rtdWebSocketManager.isSocketClosed) {
|
|
629
|
+
this.services.rtdWebSocketManager.close(false, 'Unregistering the RTD websocket');
|
|
630
|
+
}
|
|
631
|
+
|
|
586
632
|
// Clear any cached agent configuration
|
|
587
633
|
this.agentConfig = null;
|
|
588
634
|
|
|
@@ -703,98 +749,110 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
703
749
|
* @private
|
|
704
750
|
*/
|
|
705
751
|
private async connectWebsocket() {
|
|
706
|
-
LoggerProxy.
|
|
752
|
+
LoggerProxy.log('Connecting to websocket', {
|
|
707
753
|
module: CC_FILE,
|
|
708
754
|
method: METHODS.CONNECT_WEBSOCKET,
|
|
709
755
|
});
|
|
756
|
+
|
|
710
757
|
try {
|
|
711
|
-
|
|
712
|
-
.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
})
|
|
716
|
-
.then(async (data: WelcomeEvent) => {
|
|
717
|
-
const agentId = data.agentId;
|
|
718
|
-
const orgId = this.$webex.credentials.getOrgId();
|
|
719
|
-
this.agentConfig = await this.services.config.getAgentConfig(orgId, agentId);
|
|
720
|
-
LoggerProxy.log(`Agent config is fetched successfully`, {
|
|
721
|
-
module: CC_FILE,
|
|
722
|
-
method: METHODS.CONNECT_WEBSOCKET,
|
|
723
|
-
});
|
|
758
|
+
const data = (await this.services.webSocketManager.initWebSocket({
|
|
759
|
+
body: this.getConnectionConfig(),
|
|
760
|
+
resource: SUBSCRIBE_API,
|
|
761
|
+
})) as WelcomeEvent;
|
|
724
762
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
763
|
+
const agentId = data.agentId;
|
|
764
|
+
const orgId = this.$webex.credentials.getOrgId();
|
|
765
|
+
this.agentConfig = await this.services.config.getAgentConfig(orgId, agentId);
|
|
766
|
+
|
|
767
|
+
LoggerProxy.log(`Agent config is fetched successfully`, {
|
|
768
|
+
module: CC_FILE,
|
|
769
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const configFlags: ConfigFlags = {
|
|
773
|
+
isEndTaskEnabled: this.agentConfig.isEndTaskEnabled,
|
|
774
|
+
isEndConsultEnabled: this.agentConfig.isEndConsultEnabled,
|
|
775
|
+
webRtcEnabled: this.agentConfig.webRtcEnabled,
|
|
776
|
+
autoWrapup: this.agentConfig.wrapUpData?.wrapUpProps?.autoWrapup ?? false,
|
|
777
|
+
aiFeature: this.agentConfig.aiFeature,
|
|
778
|
+
};
|
|
779
|
+
this.taskManager.setConfigFlags(configFlags);
|
|
780
|
+
// TODO: Make profile a singleton to make it available throughout app/sdk so we dont need to inject info everywhere
|
|
781
|
+
this.taskManager.setWrapupData(this.agentConfig.wrapUpData);
|
|
782
|
+
this.taskManager.setAgentId(this.agentConfig.agentId);
|
|
783
|
+
this.taskManager.setWebRtcEnabled(this.agentConfig.webRtcEnabled);
|
|
784
|
+
this.apiAIAssistant.setAIFeatureFlags(this.agentConfig.aiFeature);
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* RTD websocket currently supports realtime transcripts and suggested responses.
|
|
788
|
+
* Extend this condition when additional AI RTD features are introduced.
|
|
789
|
+
*/
|
|
790
|
+
if (
|
|
791
|
+
this.agentConfig.aiFeature?.realtimeTranscripts?.enable ||
|
|
792
|
+
this.agentConfig.aiFeature?.suggestedResponses?.enable
|
|
793
|
+
) {
|
|
794
|
+
LoggerProxy.info('Connecting to RTD websocket', {
|
|
795
|
+
module: CC_FILE,
|
|
796
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
this.services.rtdWebSocketManager
|
|
800
|
+
.initWebSocket({
|
|
801
|
+
body: this.getConnectionConfig(),
|
|
802
|
+
resource: RTD_SUBSCRIBE_API,
|
|
803
|
+
})
|
|
804
|
+
.then(() => {
|
|
805
|
+
LoggerProxy.log('RTD websocket connected successfully', {
|
|
806
|
+
module: CC_FILE,
|
|
807
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
808
|
+
});
|
|
809
|
+
this.services.rtdWebSocketManager.on('message', this.handleRTDWebsocketMessage);
|
|
810
|
+
})
|
|
811
|
+
.catch((error) => {
|
|
812
|
+
LoggerProxy.error(`Error connecting to RTD websocket ${error}`, {
|
|
747
813
|
module: CC_FILE,
|
|
748
814
|
method: METHODS.CONNECT_WEBSOCKET,
|
|
749
815
|
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
750
818
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
this.$webex.internal.mercury
|
|
775
|
-
.connect()
|
|
776
|
-
.then(() => {
|
|
777
|
-
LoggerProxy.log('Authentication: webex.internal.mercury.connect successful', {
|
|
778
|
-
module: CC_FILE,
|
|
779
|
-
method: METHODS.CONNECT_WEBSOCKET,
|
|
780
|
-
});
|
|
781
|
-
})
|
|
782
|
-
.catch((error) => {
|
|
783
|
-
LoggerProxy.error(`Error occurred during mercury.connect() ${error}`, {
|
|
784
|
-
module: CC_FILE,
|
|
785
|
-
method: METHODS.CONNECT_WEBSOCKET,
|
|
786
|
-
});
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
if (this.$config && this.$config.allowAutomatedRelogin) {
|
|
790
|
-
await this.silentRelogin();
|
|
819
|
+
if (
|
|
820
|
+
this.agentConfig.webRtcEnabled &&
|
|
821
|
+
this.agentConfig.loginVoiceOptions.includes(LoginOption.BROWSER) &&
|
|
822
|
+
!this.isWebRTCRegistrationDisabled()
|
|
823
|
+
) {
|
|
824
|
+
try {
|
|
825
|
+
await this.$webex.internal.mercury.connect();
|
|
826
|
+
LoggerProxy.log('Authentication: webex.internal.mercury.connect successful', {
|
|
827
|
+
module: CC_FILE,
|
|
828
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
829
|
+
});
|
|
830
|
+
} catch (error) {
|
|
831
|
+
LoggerProxy.error(`Error occurred during mercury.connect() ${error}`, {
|
|
832
|
+
module: CC_FILE,
|
|
833
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
} else if (this.isWebRTCRegistrationDisabled()) {
|
|
837
|
+
LoggerProxy.info(
|
|
838
|
+
'Skipping Mobius registration because disableWebRTCRegistration is enabled',
|
|
839
|
+
{
|
|
840
|
+
module: CC_FILE,
|
|
841
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
791
842
|
}
|
|
843
|
+
);
|
|
844
|
+
}
|
|
792
845
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
846
|
+
if (this.$config && this.$config.allowAutomatedRelogin) {
|
|
847
|
+
await this.silentRelogin();
|
|
848
|
+
} else {
|
|
849
|
+
LoggerProxy.log('Skipping silent relogin: allowAutomatedRelogin is disabled', {
|
|
850
|
+
module: CC_FILE,
|
|
851
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
797
852
|
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return this.agentConfig;
|
|
798
856
|
} catch (error) {
|
|
799
857
|
LoggerProxy.error(`Error during register: ${error}`, {
|
|
800
858
|
module: CC_FILE,
|
|
@@ -834,22 +892,37 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
834
892
|
* ```
|
|
835
893
|
*/
|
|
836
894
|
public async stationLogin(data: AgentLogin): Promise<StationLoginResponse> {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
method: METHODS.STATION_LOGIN,
|
|
840
|
-
});
|
|
895
|
+
const loggerContext = {module: CC_FILE, method: METHODS.STATION_LOGIN};
|
|
896
|
+
|
|
841
897
|
try {
|
|
898
|
+
LoggerProxy.log(
|
|
899
|
+
`Starting agent station login | loginOption: ${data?.loginOption} teamId: ${data?.teamId}`,
|
|
900
|
+
loggerContext
|
|
901
|
+
);
|
|
842
902
|
this.metricsManager.timeEvent([
|
|
843
903
|
METRIC_EVENT_NAMES.STATION_LOGIN_SUCCESS,
|
|
844
904
|
METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
|
|
845
905
|
]);
|
|
846
906
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
907
|
+
const dialPlanEntries = this.agentConfig?.dialPlan?.dialPlanEntity ?? [];
|
|
908
|
+
if (data.loginOption === LoginOption.AGENT_DN) {
|
|
909
|
+
LoggerProxy.log(
|
|
910
|
+
`Validating dial number | dialPlanEnabled: ${!!this.agentConfig
|
|
911
|
+
?.dialPlan} | dialPlanEntryCount: ${dialPlanEntries.length}`,
|
|
912
|
+
loggerContext
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
if (!isValidDialNumber(data.dialNumber, dialPlanEntries)) {
|
|
916
|
+
LoggerProxy.log(
|
|
917
|
+
`Dial number validation failed | dialNumber: ${data.dialNumber} | dialPlanEntryCount: ${dialPlanEntries.length}`,
|
|
918
|
+
loggerContext
|
|
919
|
+
);
|
|
920
|
+
const error = new Error('INVALID_DIAL_NUMBER');
|
|
921
|
+
// @ts-ignore - adding custom key to the error object
|
|
922
|
+
error.details = {data: {reason: 'INVALID_DIAL_NUMBER'}} as Failure;
|
|
851
923
|
|
|
852
|
-
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
853
926
|
}
|
|
854
927
|
|
|
855
928
|
const loginResponse = await this.services.agent.stationLogin({
|
|
@@ -868,8 +941,24 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
868
941
|
},
|
|
869
942
|
});
|
|
870
943
|
|
|
871
|
-
if (
|
|
944
|
+
if (
|
|
945
|
+
this.agentConfig.webRtcEnabled &&
|
|
946
|
+
data.loginOption === LoginOption.BROWSER &&
|
|
947
|
+
!this.isWebRTCRegistrationDisabled()
|
|
948
|
+
) {
|
|
872
949
|
await this.webCallingService.registerWebCallingLine();
|
|
950
|
+
} else if (
|
|
951
|
+
this.agentConfig.webRtcEnabled &&
|
|
952
|
+
data.loginOption === LoginOption.BROWSER &&
|
|
953
|
+
this.isWebRTCRegistrationDisabled()
|
|
954
|
+
) {
|
|
955
|
+
LoggerProxy.info(
|
|
956
|
+
'Skipping web calling line registration because disableWebRTCRegistration is enabled',
|
|
957
|
+
{
|
|
958
|
+
module: CC_FILE,
|
|
959
|
+
method: METHODS.STATION_LOGIN,
|
|
960
|
+
}
|
|
961
|
+
);
|
|
873
962
|
}
|
|
874
963
|
|
|
875
964
|
const resp = await loginResponse;
|
|
@@ -1231,9 +1320,29 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1231
1320
|
}
|
|
1232
1321
|
};
|
|
1233
1322
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1323
|
+
/**
|
|
1324
|
+
* Checks whether Mobius/WebRTC registration should be skipped by config
|
|
1325
|
+
* @returns {boolean} True when browser WebRTC registration is disabled
|
|
1326
|
+
* @private
|
|
1327
|
+
*/
|
|
1328
|
+
private isWebRTCRegistrationDisabled(): boolean {
|
|
1329
|
+
return this.$config?.disableWebRTCRegistration === true;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Validates contact-center plugin configuration before service initialization
|
|
1334
|
+
* @private
|
|
1335
|
+
*/
|
|
1336
|
+
private validatePluginConfig(): void {
|
|
1337
|
+
if (
|
|
1338
|
+
this.$config?.disableWebRTCRegistration === true &&
|
|
1339
|
+
this.$config?.allowMultiLogin === false
|
|
1340
|
+
) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
'Invalid Contact Center configuration: disableWebRTCRegistration cannot be true when allowMultiLogin is false. Enable allowMultiLogin or allow WebRTC registration so an SDK instance can receive Mobius/WebRTC task events.'
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1237
1346
|
|
|
1238
1347
|
/**
|
|
1239
1348
|
* Initializes event listeners for the Contact Center service
|
|
@@ -1266,13 +1375,13 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1266
1375
|
private async handleConnectionLost(msg: ConnectionLostDetails): Promise<void> {
|
|
1267
1376
|
if (msg.isConnectionLost) {
|
|
1268
1377
|
// TODO: Emit an event saying connection is lost
|
|
1269
|
-
LoggerProxy.
|
|
1378
|
+
LoggerProxy.log('event=handleConnectionLost | Connection lost', {
|
|
1270
1379
|
module: CC_FILE,
|
|
1271
1380
|
method: METHODS.HANDLE_CONNECTION_LOST,
|
|
1272
1381
|
});
|
|
1273
1382
|
} else if (msg.isSocketReconnected) {
|
|
1274
1383
|
// TODO: Emit an event saying connection is re-estabilished
|
|
1275
|
-
LoggerProxy.
|
|
1384
|
+
LoggerProxy.log(
|
|
1276
1385
|
'event=handleConnectionReconnect | Connection reconnected attempting to request silent relogin',
|
|
1277
1386
|
{module: CC_FILE, method: METHODS.HANDLE_CONNECTION_LOST}
|
|
1278
1387
|
);
|
|
@@ -1287,7 +1396,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1287
1396
|
* @private
|
|
1288
1397
|
*/
|
|
1289
1398
|
private async silentRelogin(): Promise<void> {
|
|
1290
|
-
LoggerProxy.
|
|
1399
|
+
LoggerProxy.log('Starting silent relogin process', {
|
|
1291
1400
|
module: CC_FILE,
|
|
1292
1401
|
method: METHODS.SILENT_RELOGIN,
|
|
1293
1402
|
});
|
|
@@ -1309,7 +1418,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1309
1418
|
await this.handleDeviceType(deviceType as LoginOption, dn);
|
|
1310
1419
|
|
|
1311
1420
|
if (lastStateChangeReason === 'agent-wss-disconnect') {
|
|
1312
|
-
LoggerProxy.
|
|
1421
|
+
LoggerProxy.log(
|
|
1313
1422
|
'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
|
|
1314
1423
|
{module: CC_FILE, method: METHODS.SILENT_RELOGIN}
|
|
1315
1424
|
);
|
|
@@ -1338,8 +1447,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1338
1447
|
}
|
|
1339
1448
|
this.agentConfig.lastStateAuxCodeId = auxCodeId;
|
|
1340
1449
|
this.agentConfig.isAgentLoggedIn = true;
|
|
1341
|
-
// TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
|
|
1342
|
-
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
1343
1450
|
|
|
1344
1451
|
LoggerProxy.log(
|
|
1345
1452
|
`Silent relogin process completed successfully with login Option: ${reLoginResponse.data.deviceType} teamId: ${reLoginResponse.data.teamId}`,
|
|
@@ -1380,6 +1487,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1380
1487
|
this.agentConfig.deviceType = deviceType;
|
|
1381
1488
|
switch (deviceType) {
|
|
1382
1489
|
case LoginOption.BROWSER:
|
|
1490
|
+
if (this.isWebRTCRegistrationDisabled()) {
|
|
1491
|
+
LoggerProxy.info(
|
|
1492
|
+
'Skipping web calling line registration because disableWebRTCRegistration is enabled',
|
|
1493
|
+
{
|
|
1494
|
+
module: CC_FILE,
|
|
1495
|
+
method: METHODS.HANDLE_DEVICE_TYPE,
|
|
1496
|
+
}
|
|
1497
|
+
);
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1383
1500
|
try {
|
|
1384
1501
|
await this.webCallingService.registerWebCallingLine();
|
|
1385
1502
|
} catch (error) {
|
|
@@ -1572,6 +1689,239 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1572
1689
|
}
|
|
1573
1690
|
}
|
|
1574
1691
|
|
|
1692
|
+
/**
|
|
1693
|
+
* Accepts a campaign preview contact, initiating the outbound call.
|
|
1694
|
+
*
|
|
1695
|
+
* When a campaign manager reserves a contact for an agent, the agent receives an
|
|
1696
|
+
* `AgentOfferCampaignReservation` event. The agent can then accept the preview contact
|
|
1697
|
+
* to initiate the outbound call.
|
|
1698
|
+
*
|
|
1699
|
+
* @param {PreviewContactPayload} payload - The preview contact payload containing interactionId and campaignId (campaign name, not UUID).
|
|
1700
|
+
* @returns {Promise<TaskResponse>} Promise resolving with agent contact on success.
|
|
1701
|
+
* @throws {Error} If the operation fails (network error, customer unavailable, etc.)
|
|
1702
|
+
*
|
|
1703
|
+
* @example
|
|
1704
|
+
* ```typescript
|
|
1705
|
+
* webex.cc.on('task:campaignPreviewReservation', async (task) => {
|
|
1706
|
+
* const { interactionId } = task.data;
|
|
1707
|
+
* // campaignId is the campaign name (e.g. "MyCampaign"), not a UUID
|
|
1708
|
+
* const campaignId = task.data.interaction.callProcessingDetails.campaignId;
|
|
1709
|
+
*
|
|
1710
|
+
* const result = await webex.cc.acceptPreviewContact({ interactionId, campaignId });
|
|
1711
|
+
* });
|
|
1712
|
+
* ```
|
|
1713
|
+
*/
|
|
1714
|
+
public async acceptPreviewContact(payload: PreviewContactPayload): Promise<TaskResponse> {
|
|
1715
|
+
LoggerProxy.info('Accepting campaign preview contact', {
|
|
1716
|
+
module: CC_FILE,
|
|
1717
|
+
method: METHODS.ACCEPT_PREVIEW_CONTACT,
|
|
1718
|
+
});
|
|
1719
|
+
try {
|
|
1720
|
+
this.metricsManager.timeEvent([
|
|
1721
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_ACCEPT_SUCCESS,
|
|
1722
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
|
|
1723
|
+
]);
|
|
1724
|
+
|
|
1725
|
+
const result = await this.services.dialer.acceptPreviewContact({data: payload});
|
|
1726
|
+
|
|
1727
|
+
this.metricsManager.trackEvent(
|
|
1728
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_ACCEPT_SUCCESS,
|
|
1729
|
+
{
|
|
1730
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1731
|
+
interactionId: payload.interactionId,
|
|
1732
|
+
campaignId: payload.campaignId,
|
|
1733
|
+
},
|
|
1734
|
+
['behavioral', 'business', 'operational']
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
LoggerProxy.log('Campaign preview contact accepted successfully', {
|
|
1738
|
+
module: CC_FILE,
|
|
1739
|
+
method: METHODS.ACCEPT_PREVIEW_CONTACT,
|
|
1740
|
+
trackingId: result.trackingId,
|
|
1741
|
+
interactionId: payload.interactionId,
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
return result;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
const failure = error.details as Failure;
|
|
1747
|
+
this.metricsManager.trackEvent(
|
|
1748
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
|
|
1749
|
+
{
|
|
1750
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure),
|
|
1751
|
+
interactionId: payload.interactionId,
|
|
1752
|
+
campaignId: payload.campaignId,
|
|
1753
|
+
},
|
|
1754
|
+
['behavioral', 'business', 'operational']
|
|
1755
|
+
);
|
|
1756
|
+
const {error: detailedError} = getErrorDetails(
|
|
1757
|
+
error,
|
|
1758
|
+
METHODS.ACCEPT_PREVIEW_CONTACT,
|
|
1759
|
+
CC_FILE
|
|
1760
|
+
);
|
|
1761
|
+
throw detailedError;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Skips a campaign preview contact, requesting the next contact from the campaign.
|
|
1767
|
+
*
|
|
1768
|
+
* When a campaign manager reserves a contact for an agent, the agent receives an
|
|
1769
|
+
* `AgentOfferCampaignReservation` event. Instead of accepting, the agent can skip the
|
|
1770
|
+
* preview contact to move to the next contact in the campaign.
|
|
1771
|
+
*
|
|
1772
|
+
* @param {PreviewContactPayload} payload - The preview contact payload containing interactionId and campaignId (campaign name, not UUID).
|
|
1773
|
+
* @returns {Promise<TaskResponse>} Promise resolving with agent contact on success.
|
|
1774
|
+
* @throws {Error} If the operation fails (network error, etc.)
|
|
1775
|
+
*
|
|
1776
|
+
* @example
|
|
1777
|
+
* ```typescript
|
|
1778
|
+
* webex.cc.on('task:campaignPreviewReservation', async (task) => {
|
|
1779
|
+
* const { interactionId } = task.data;
|
|
1780
|
+
* const campaignId = task.data.interaction.callProcessingDetails.campaignId;
|
|
1781
|
+
*
|
|
1782
|
+
* const result = await webex.cc.skipPreviewContact({ interactionId, campaignId });
|
|
1783
|
+
* });
|
|
1784
|
+
* ```
|
|
1785
|
+
*/
|
|
1786
|
+
public async skipPreviewContact(payload: PreviewContactPayload): Promise<TaskResponse> {
|
|
1787
|
+
const task = this.taskManager.getTask(payload.interactionId);
|
|
1788
|
+
if (task?.data?.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled === 'true') {
|
|
1789
|
+
LoggerProxy.warn('Skip action is disabled for this campaign preview contact', {
|
|
1790
|
+
module: CC_FILE,
|
|
1791
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1792
|
+
interactionId: payload.interactionId,
|
|
1793
|
+
});
|
|
1794
|
+
throw new Error('Skip action is disabled for this campaign preview contact');
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
LoggerProxy.info('Skipping campaign preview contact', {
|
|
1798
|
+
module: CC_FILE,
|
|
1799
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1800
|
+
});
|
|
1801
|
+
try {
|
|
1802
|
+
this.metricsManager.timeEvent([
|
|
1803
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_SUCCESS,
|
|
1804
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
1805
|
+
]);
|
|
1806
|
+
|
|
1807
|
+
const result = await this.services.dialer.skipPreviewContact({data: payload});
|
|
1808
|
+
|
|
1809
|
+
this.metricsManager.trackEvent(
|
|
1810
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_SUCCESS,
|
|
1811
|
+
{
|
|
1812
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1813
|
+
interactionId: payload.interactionId,
|
|
1814
|
+
campaignId: payload.campaignId,
|
|
1815
|
+
},
|
|
1816
|
+
['behavioral', 'business', 'operational']
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
LoggerProxy.log('Campaign preview contact skipped successfully', {
|
|
1820
|
+
module: CC_FILE,
|
|
1821
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1822
|
+
trackingId: result.trackingId,
|
|
1823
|
+
interactionId: payload.interactionId,
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
return result;
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
const failure = error.details as Failure;
|
|
1829
|
+
this.metricsManager.trackEvent(
|
|
1830
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
1831
|
+
{
|
|
1832
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure),
|
|
1833
|
+
interactionId: payload.interactionId,
|
|
1834
|
+
campaignId: payload.campaignId,
|
|
1835
|
+
},
|
|
1836
|
+
['behavioral', 'business', 'operational']
|
|
1837
|
+
);
|
|
1838
|
+
const {error: detailedError} = getErrorDetails(error, METHODS.SKIP_PREVIEW_CONTACT, CC_FILE);
|
|
1839
|
+
throw detailedError;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Removes a campaign preview contact from the campaign list entirely.
|
|
1845
|
+
*
|
|
1846
|
+
* When a campaign manager reserves a contact for an agent, the agent receives an
|
|
1847
|
+
* `AgentOfferCampaignReservation` event. Instead of accepting or skipping, the agent can
|
|
1848
|
+
* remove the preview contact to permanently take it out of the campaign contact list.
|
|
1849
|
+
*
|
|
1850
|
+
* @param {PreviewContactPayload} payload - The preview contact payload containing interactionId and campaignId (campaign name, not UUID).
|
|
1851
|
+
* @returns {Promise<TaskResponse>} Promise resolving with agent contact on success.
|
|
1852
|
+
* @throws {Error} If the operation fails (network error, etc.)
|
|
1853
|
+
*
|
|
1854
|
+
* @example
|
|
1855
|
+
* ```typescript
|
|
1856
|
+
* webex.cc.on('task:campaignPreviewReservation', async (task) => {
|
|
1857
|
+
* const { interactionId } = task.data;
|
|
1858
|
+
* const campaignId = task.data.interaction.callProcessingDetails.campaignId;
|
|
1859
|
+
*
|
|
1860
|
+
* const result = await webex.cc.removePreviewContact({ interactionId, campaignId });
|
|
1861
|
+
* });
|
|
1862
|
+
* ```
|
|
1863
|
+
*/
|
|
1864
|
+
public async removePreviewContact(payload: PreviewContactPayload): Promise<TaskResponse> {
|
|
1865
|
+
const task = this.taskManager.getTask(payload.interactionId);
|
|
1866
|
+
if (task?.data?.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled === 'true') {
|
|
1867
|
+
LoggerProxy.warn('Remove action is disabled for this campaign preview contact', {
|
|
1868
|
+
module: CC_FILE,
|
|
1869
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1870
|
+
interactionId: payload.interactionId,
|
|
1871
|
+
});
|
|
1872
|
+
throw new Error('Remove action is disabled for this campaign preview contact');
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
LoggerProxy.info('Removing campaign preview contact', {
|
|
1876
|
+
module: CC_FILE,
|
|
1877
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1878
|
+
});
|
|
1879
|
+
try {
|
|
1880
|
+
this.metricsManager.timeEvent([
|
|
1881
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_SUCCESS,
|
|
1882
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
1883
|
+
]);
|
|
1884
|
+
|
|
1885
|
+
const result = await this.services.dialer.removePreviewContact({data: payload});
|
|
1886
|
+
|
|
1887
|
+
this.metricsManager.trackEvent(
|
|
1888
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_SUCCESS,
|
|
1889
|
+
{
|
|
1890
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1891
|
+
interactionId: payload.interactionId,
|
|
1892
|
+
campaignId: payload.campaignId,
|
|
1893
|
+
},
|
|
1894
|
+
['behavioral', 'business', 'operational']
|
|
1895
|
+
);
|
|
1896
|
+
|
|
1897
|
+
LoggerProxy.log('Campaign preview contact removed successfully', {
|
|
1898
|
+
module: CC_FILE,
|
|
1899
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1900
|
+
trackingId: result.trackingId,
|
|
1901
|
+
interactionId: payload.interactionId,
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
return result;
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
const failure = error.details as Failure;
|
|
1907
|
+
this.metricsManager.trackEvent(
|
|
1908
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
1909
|
+
{
|
|
1910
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure),
|
|
1911
|
+
interactionId: payload.interactionId,
|
|
1912
|
+
campaignId: payload.campaignId,
|
|
1913
|
+
},
|
|
1914
|
+
['behavioral', 'business', 'operational']
|
|
1915
|
+
);
|
|
1916
|
+
const {error: detailedError} = getErrorDetails(
|
|
1917
|
+
error,
|
|
1918
|
+
METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1919
|
+
CC_FILE
|
|
1920
|
+
);
|
|
1921
|
+
throw detailedError;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1575
1925
|
/**
|
|
1576
1926
|
* Fetches outdial ANI (Automatic Number Identification) entries for an outdial ANI ID.
|
|
1577
1927
|
*
|