@webex/contact-center 3.12.0-next.6 → 3.12.0-next.60
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 +161 -21
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/metrics/behavioral-events.js +26 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +4 -0
- package/dist/metrics/constants.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/constants.js +1 -1
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/types.js +4 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +37 -9
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/task/TaskManager.js +90 -8
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/constants.js +3 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +78 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/index.js +7 -2
- package/dist/services/task/index.js.map +1 -1
- package/dist/services/task/types.js +44 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/types/cc.d.ts +44 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/services/config/types.d.ts +10 -1
- package/dist/types/services/core/Err.d.ts +4 -0
- package/dist/types/services/core/Utils.d.ts +10 -3
- package/dist/types/services/task/constants.d.ts +2 -0
- package/dist/types/services/task/dialer.d.ts +30 -0
- package/dist/types/services/task/types.d.ts +53 -1
- package/dist/webex.js +1 -1
- package/package.json +9 -9
- package/src/cc.ts +196 -22
- package/src/constants.ts +2 -0
- package/src/metrics/behavioral-events.ts +28 -0
- package/src/metrics/constants.ts +4 -0
- package/src/services/config/Util.ts +1 -1
- package/src/services/config/constants.ts +1 -1
- package/src/services/config/types.ts +6 -1
- package/src/services/core/Err.ts +2 -0
- package/src/services/core/Utils.ts +43 -8
- package/src/services/task/TaskManager.ts +102 -22
- package/src/services/task/constants.ts +2 -0
- package/src/services/task/dialer.ts +80 -0
- package/src/services/task/index.ts +7 -2
- package/src/services/task/types.ts +56 -0
- package/test/unit/spec/cc.ts +154 -20
- package/test/unit/spec/services/config/index.ts +3 -3
- package/test/unit/spec/services/core/Utils.ts +90 -7
- package/test/unit/spec/services/task/TaskManager.ts +238 -7
- package/test/unit/spec/services/task/dialer.ts +190 -0
- package/test/unit/spec/services/task/index.ts +21 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
package/src/cc.ts
CHANGED
|
@@ -369,7 +369,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
369
369
|
webex: this.$webex,
|
|
370
370
|
connectionConfig: this.getConnectionConfig(),
|
|
371
371
|
});
|
|
372
|
-
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
373
372
|
|
|
374
373
|
this.webCallingService = new WebCallingService(this.$webex);
|
|
375
374
|
this.apiAIAssistant = new ApiAIAssistant(this.$webex);
|
|
@@ -487,7 +486,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
487
486
|
* ```
|
|
488
487
|
*/
|
|
489
488
|
public async register(): Promise<Profile> {
|
|
490
|
-
LoggerProxy.
|
|
489
|
+
LoggerProxy.log('Starting CC SDK registration', {
|
|
491
490
|
module: CC_FILE,
|
|
492
491
|
method: METHODS.REGISTER,
|
|
493
492
|
});
|
|
@@ -497,6 +496,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
497
496
|
METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_FAILED,
|
|
498
497
|
]);
|
|
499
498
|
this.setupEventListeners();
|
|
499
|
+
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
500
500
|
|
|
501
501
|
const resp = await this.connectWebsocket();
|
|
502
502
|
// Ensure 'dn' is always populated from 'defaultDn'
|
|
@@ -731,7 +731,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
731
731
|
* @private
|
|
732
732
|
*/
|
|
733
733
|
private async connectWebsocket() {
|
|
734
|
-
LoggerProxy.
|
|
734
|
+
LoggerProxy.log('Connecting to websocket', {
|
|
735
735
|
module: CC_FILE,
|
|
736
736
|
method: METHODS.CONNECT_WEBSOCKET,
|
|
737
737
|
});
|
|
@@ -758,7 +758,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
758
758
|
this.apiAIAssistant.setAIFeatureFlags(this.agentConfig.aiFeature);
|
|
759
759
|
|
|
760
760
|
if (this.agentConfig.aiFeature?.realtimeTranscripts?.enable) {
|
|
761
|
-
LoggerProxy.
|
|
761
|
+
LoggerProxy.log('Connecting to RTD websocket', {
|
|
762
762
|
module: CC_FILE,
|
|
763
763
|
method: METHODS.CONNECT_WEBSOCKET,
|
|
764
764
|
});
|
|
@@ -803,6 +803,11 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
803
803
|
|
|
804
804
|
if (this.$config && this.$config.allowAutomatedRelogin) {
|
|
805
805
|
await this.silentRelogin();
|
|
806
|
+
} else {
|
|
807
|
+
LoggerProxy.log('Skipping silent relogin: allowAutomatedRelogin is disabled', {
|
|
808
|
+
module: CC_FILE,
|
|
809
|
+
method: METHODS.CONNECT_WEBSOCKET,
|
|
810
|
+
});
|
|
806
811
|
}
|
|
807
812
|
|
|
808
813
|
return this.agentConfig;
|
|
@@ -845,26 +850,37 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
845
850
|
* ```
|
|
846
851
|
*/
|
|
847
852
|
public async stationLogin(data: AgentLogin): Promise<StationLoginResponse> {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
method: METHODS.STATION_LOGIN,
|
|
851
|
-
});
|
|
853
|
+
const loggerContext = {module: CC_FILE, method: METHODS.STATION_LOGIN};
|
|
854
|
+
|
|
852
855
|
try {
|
|
856
|
+
LoggerProxy.log(
|
|
857
|
+
`Starting agent station login | loginOption: ${data?.loginOption} teamId: ${data?.teamId}`,
|
|
858
|
+
loggerContext
|
|
859
|
+
);
|
|
853
860
|
this.metricsManager.timeEvent([
|
|
854
861
|
METRIC_EVENT_NAMES.STATION_LOGIN_SUCCESS,
|
|
855
862
|
METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
|
|
856
863
|
]);
|
|
857
864
|
|
|
858
865
|
const dialPlanEntries = this.agentConfig?.dialPlan?.dialPlanEntity ?? [];
|
|
859
|
-
if (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
error.details = {data: {reason: 'INVALID_DIAL_NUMBER'}} as Failure;
|
|
866
|
+
if (data.loginOption === LoginOption.AGENT_DN) {
|
|
867
|
+
LoggerProxy.log(
|
|
868
|
+
`Validating dial number | dialPlanEnabled: ${!!this.agentConfig
|
|
869
|
+
?.dialPlan} | dialPlanEntryCount: ${dialPlanEntries.length}`,
|
|
870
|
+
loggerContext
|
|
871
|
+
);
|
|
866
872
|
|
|
867
|
-
|
|
873
|
+
if (!isValidDialNumber(data.dialNumber, dialPlanEntries)) {
|
|
874
|
+
LoggerProxy.log(
|
|
875
|
+
`Dial number validation failed | dialNumber: ${data.dialNumber} | dialPlanEntryCount: ${dialPlanEntries.length}`,
|
|
876
|
+
loggerContext
|
|
877
|
+
);
|
|
878
|
+
const error = new Error('INVALID_DIAL_NUMBER');
|
|
879
|
+
// @ts-ignore - adding custom key to the error object
|
|
880
|
+
error.details = {data: {reason: 'INVALID_DIAL_NUMBER'}} as Failure;
|
|
881
|
+
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
868
884
|
}
|
|
869
885
|
|
|
870
886
|
const loginResponse = await this.services.agent.stationLogin({
|
|
@@ -1277,13 +1293,13 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1277
1293
|
private async handleConnectionLost(msg: ConnectionLostDetails): Promise<void> {
|
|
1278
1294
|
if (msg.isConnectionLost) {
|
|
1279
1295
|
// TODO: Emit an event saying connection is lost
|
|
1280
|
-
LoggerProxy.
|
|
1296
|
+
LoggerProxy.log('event=handleConnectionLost | Connection lost', {
|
|
1281
1297
|
module: CC_FILE,
|
|
1282
1298
|
method: METHODS.HANDLE_CONNECTION_LOST,
|
|
1283
1299
|
});
|
|
1284
1300
|
} else if (msg.isSocketReconnected) {
|
|
1285
1301
|
// TODO: Emit an event saying connection is re-estabilished
|
|
1286
|
-
LoggerProxy.
|
|
1302
|
+
LoggerProxy.log(
|
|
1287
1303
|
'event=handleConnectionReconnect | Connection reconnected attempting to request silent relogin',
|
|
1288
1304
|
{module: CC_FILE, method: METHODS.HANDLE_CONNECTION_LOST}
|
|
1289
1305
|
);
|
|
@@ -1298,7 +1314,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1298
1314
|
* @private
|
|
1299
1315
|
*/
|
|
1300
1316
|
private async silentRelogin(): Promise<void> {
|
|
1301
|
-
LoggerProxy.
|
|
1317
|
+
LoggerProxy.log('Starting silent relogin process', {
|
|
1302
1318
|
module: CC_FILE,
|
|
1303
1319
|
method: METHODS.SILENT_RELOGIN,
|
|
1304
1320
|
});
|
|
@@ -1320,7 +1336,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1320
1336
|
await this.handleDeviceType(deviceType as LoginOption, dn);
|
|
1321
1337
|
|
|
1322
1338
|
if (lastStateChangeReason === 'agent-wss-disconnect') {
|
|
1323
|
-
LoggerProxy.
|
|
1339
|
+
LoggerProxy.log(
|
|
1324
1340
|
'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
|
|
1325
1341
|
{module: CC_FILE, method: METHODS.SILENT_RELOGIN}
|
|
1326
1342
|
);
|
|
@@ -1349,8 +1365,6 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1349
1365
|
}
|
|
1350
1366
|
this.agentConfig.lastStateAuxCodeId = auxCodeId;
|
|
1351
1367
|
this.agentConfig.isAgentLoggedIn = true;
|
|
1352
|
-
// TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
|
|
1353
|
-
this.services.webSocketManager.on('message', this.handleWebsocketMessage);
|
|
1354
1368
|
|
|
1355
1369
|
LoggerProxy.log(
|
|
1356
1370
|
`Silent relogin process completed successfully with login Option: ${reLoginResponse.data.deviceType} teamId: ${reLoginResponse.data.teamId}`,
|
|
@@ -1656,6 +1670,166 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
|
|
|
1656
1670
|
}
|
|
1657
1671
|
}
|
|
1658
1672
|
|
|
1673
|
+
/**
|
|
1674
|
+
* Skips a campaign preview contact, requesting the next contact from the campaign.
|
|
1675
|
+
*
|
|
1676
|
+
* When a campaign manager reserves a contact for an agent, the agent receives an
|
|
1677
|
+
* `AgentOfferCampaignReservation` event. Instead of accepting, the agent can skip the
|
|
1678
|
+
* preview contact to move to the next contact in the campaign.
|
|
1679
|
+
*
|
|
1680
|
+
* @param {PreviewContactPayload} payload - The preview contact payload containing interactionId and campaignId (campaign name, not UUID).
|
|
1681
|
+
* @returns {Promise<TaskResponse>} Promise resolving with agent contact on success.
|
|
1682
|
+
* @throws {Error} If the operation fails (network error, etc.)
|
|
1683
|
+
*
|
|
1684
|
+
* @example
|
|
1685
|
+
* ```typescript
|
|
1686
|
+
* webex.cc.on('task:campaignPreviewReservation', async (task) => {
|
|
1687
|
+
* const { interactionId } = task.data;
|
|
1688
|
+
* const campaignId = task.data.interaction.callProcessingDetails.campaignId;
|
|
1689
|
+
*
|
|
1690
|
+
* const result = await webex.cc.skipPreviewContact({ interactionId, campaignId });
|
|
1691
|
+
* });
|
|
1692
|
+
* ```
|
|
1693
|
+
*/
|
|
1694
|
+
public async skipPreviewContact(payload: PreviewContactPayload): Promise<TaskResponse> {
|
|
1695
|
+
const task = this.taskManager.getTask(payload.interactionId);
|
|
1696
|
+
if (task?.data?.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled === 'true') {
|
|
1697
|
+
LoggerProxy.warn('Skip action is disabled for this campaign preview contact', {
|
|
1698
|
+
module: CC_FILE,
|
|
1699
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1700
|
+
interactionId: payload.interactionId,
|
|
1701
|
+
});
|
|
1702
|
+
throw new Error('Skip action is disabled for this campaign preview contact');
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
LoggerProxy.info('Skipping campaign preview contact', {
|
|
1706
|
+
module: CC_FILE,
|
|
1707
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1708
|
+
});
|
|
1709
|
+
try {
|
|
1710
|
+
this.metricsManager.timeEvent([
|
|
1711
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_SUCCESS,
|
|
1712
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
1713
|
+
]);
|
|
1714
|
+
|
|
1715
|
+
const result = await this.services.dialer.skipPreviewContact({data: payload});
|
|
1716
|
+
|
|
1717
|
+
this.metricsManager.trackEvent(
|
|
1718
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_SUCCESS,
|
|
1719
|
+
{
|
|
1720
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1721
|
+
interactionId: payload.interactionId,
|
|
1722
|
+
campaignId: payload.campaignId,
|
|
1723
|
+
},
|
|
1724
|
+
['behavioral', 'business', 'operational']
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
LoggerProxy.log('Campaign preview contact skipped successfully', {
|
|
1728
|
+
module: CC_FILE,
|
|
1729
|
+
method: METHODS.SKIP_PREVIEW_CONTACT,
|
|
1730
|
+
trackingId: result.trackingId,
|
|
1731
|
+
interactionId: payload.interactionId,
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
return result;
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
const failure = error.details as Failure;
|
|
1737
|
+
this.metricsManager.trackEvent(
|
|
1738
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_FAILED,
|
|
1739
|
+
{
|
|
1740
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure),
|
|
1741
|
+
interactionId: payload.interactionId,
|
|
1742
|
+
campaignId: payload.campaignId,
|
|
1743
|
+
},
|
|
1744
|
+
['behavioral', 'business', 'operational']
|
|
1745
|
+
);
|
|
1746
|
+
const {error: detailedError} = getErrorDetails(error, METHODS.SKIP_PREVIEW_CONTACT, CC_FILE);
|
|
1747
|
+
throw detailedError;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Removes a campaign preview contact from the campaign list entirely.
|
|
1753
|
+
*
|
|
1754
|
+
* When a campaign manager reserves a contact for an agent, the agent receives an
|
|
1755
|
+
* `AgentOfferCampaignReservation` event. Instead of accepting or skipping, the agent can
|
|
1756
|
+
* remove the preview contact to permanently take it out of the campaign contact list.
|
|
1757
|
+
*
|
|
1758
|
+
* @param {PreviewContactPayload} payload - The preview contact payload containing interactionId and campaignId (campaign name, not UUID).
|
|
1759
|
+
* @returns {Promise<TaskResponse>} Promise resolving with agent contact on success.
|
|
1760
|
+
* @throws {Error} If the operation fails (network error, etc.)
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```typescript
|
|
1764
|
+
* webex.cc.on('task:campaignPreviewReservation', async (task) => {
|
|
1765
|
+
* const { interactionId } = task.data;
|
|
1766
|
+
* const campaignId = task.data.interaction.callProcessingDetails.campaignId;
|
|
1767
|
+
*
|
|
1768
|
+
* const result = await webex.cc.removePreviewContact({ interactionId, campaignId });
|
|
1769
|
+
* });
|
|
1770
|
+
* ```
|
|
1771
|
+
*/
|
|
1772
|
+
public async removePreviewContact(payload: PreviewContactPayload): Promise<TaskResponse> {
|
|
1773
|
+
const task = this.taskManager.getTask(payload.interactionId);
|
|
1774
|
+
if (task?.data?.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled === 'true') {
|
|
1775
|
+
LoggerProxy.warn('Remove action is disabled for this campaign preview contact', {
|
|
1776
|
+
module: CC_FILE,
|
|
1777
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1778
|
+
interactionId: payload.interactionId,
|
|
1779
|
+
});
|
|
1780
|
+
throw new Error('Remove action is disabled for this campaign preview contact');
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
LoggerProxy.info('Removing campaign preview contact', {
|
|
1784
|
+
module: CC_FILE,
|
|
1785
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1786
|
+
});
|
|
1787
|
+
try {
|
|
1788
|
+
this.metricsManager.timeEvent([
|
|
1789
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_SUCCESS,
|
|
1790
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
1791
|
+
]);
|
|
1792
|
+
|
|
1793
|
+
const result = await this.services.dialer.removePreviewContact({data: payload});
|
|
1794
|
+
|
|
1795
|
+
this.metricsManager.trackEvent(
|
|
1796
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_SUCCESS,
|
|
1797
|
+
{
|
|
1798
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
|
|
1799
|
+
interactionId: payload.interactionId,
|
|
1800
|
+
campaignId: payload.campaignId,
|
|
1801
|
+
},
|
|
1802
|
+
['behavioral', 'business', 'operational']
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
LoggerProxy.log('Campaign preview contact removed successfully', {
|
|
1806
|
+
module: CC_FILE,
|
|
1807
|
+
method: METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1808
|
+
trackingId: result.trackingId,
|
|
1809
|
+
interactionId: payload.interactionId,
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
return result;
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
const failure = error.details as Failure;
|
|
1815
|
+
this.metricsManager.trackEvent(
|
|
1816
|
+
METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_FAILED,
|
|
1817
|
+
{
|
|
1818
|
+
...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(failure),
|
|
1819
|
+
interactionId: payload.interactionId,
|
|
1820
|
+
campaignId: payload.campaignId,
|
|
1821
|
+
},
|
|
1822
|
+
['behavioral', 'business', 'operational']
|
|
1823
|
+
);
|
|
1824
|
+
const {error: detailedError} = getErrorDetails(
|
|
1825
|
+
error,
|
|
1826
|
+
METHODS.REMOVE_PREVIEW_CONTACT,
|
|
1827
|
+
CC_FILE
|
|
1828
|
+
);
|
|
1829
|
+
throw detailedError;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1659
1833
|
/**
|
|
1660
1834
|
* Fetches outdial ANI (Automatic Number Identification) entries for an outdial ANI ID.
|
|
1661
1835
|
*
|
package/src/constants.ts
CHANGED
|
@@ -50,6 +50,8 @@ export const METHODS = {
|
|
|
50
50
|
HANDLE_TASK_HYDRATE: 'handleTaskHydrate',
|
|
51
51
|
INCOMING_TASK_LISTENER: 'incomingTaskListener',
|
|
52
52
|
ACCEPT_PREVIEW_CONTACT: 'acceptPreviewContact',
|
|
53
|
+
SKIP_PREVIEW_CONTACT: 'skipPreviewContact',
|
|
54
|
+
REMOVE_PREVIEW_CONTACT: 'removePreviewContact',
|
|
53
55
|
GET_BASE_URL: 'getBaseUrl',
|
|
54
56
|
SEND_EVENT: 'sendEvent',
|
|
55
57
|
FETCH_HISTORIC_TRANSCRIPTS: 'fetchHistoricTranscripts',
|
|
@@ -449,6 +449,34 @@ const eventTaxonomyMap: Record<string, BehavioralEventTaxonomy> = {
|
|
|
449
449
|
target: 'campaign_preview_accept',
|
|
450
450
|
verb: 'fail',
|
|
451
451
|
},
|
|
452
|
+
|
|
453
|
+
// Campaign Preview Skip API Events
|
|
454
|
+
[METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_SUCCESS]: {
|
|
455
|
+
product,
|
|
456
|
+
agent: 'user',
|
|
457
|
+
target: 'campaign_preview_skip',
|
|
458
|
+
verb: 'complete',
|
|
459
|
+
},
|
|
460
|
+
[METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_SKIP_FAILED]: {
|
|
461
|
+
product,
|
|
462
|
+
agent: 'user',
|
|
463
|
+
target: 'campaign_preview_skip',
|
|
464
|
+
verb: 'fail',
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
// Campaign Preview Remove API Events
|
|
468
|
+
[METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_SUCCESS]: {
|
|
469
|
+
product,
|
|
470
|
+
agent: 'user',
|
|
471
|
+
target: 'campaign_preview_remove',
|
|
472
|
+
verb: 'complete',
|
|
473
|
+
},
|
|
474
|
+
[METRIC_EVENT_NAMES.CAMPAIGN_PREVIEW_REMOVE_FAILED]: {
|
|
475
|
+
product,
|
|
476
|
+
agent: 'user',
|
|
477
|
+
target: 'campaign_preview_remove',
|
|
478
|
+
verb: 'fail',
|
|
479
|
+
},
|
|
452
480
|
};
|
|
453
481
|
|
|
454
482
|
/**
|
package/src/metrics/constants.ts
CHANGED
|
@@ -163,6 +163,10 @@ export const METRIC_EVENT_NAMES = {
|
|
|
163
163
|
// Campaign Preview API Events
|
|
164
164
|
CAMPAIGN_PREVIEW_ACCEPT_SUCCESS: 'Campaign Preview Accept Success',
|
|
165
165
|
CAMPAIGN_PREVIEW_ACCEPT_FAILED: 'Campaign Preview Accept Failed',
|
|
166
|
+
CAMPAIGN_PREVIEW_SKIP_SUCCESS: 'Campaign Preview Skip Success',
|
|
167
|
+
CAMPAIGN_PREVIEW_SKIP_FAILED: 'Campaign Preview Skip Failed',
|
|
168
|
+
CAMPAIGN_PREVIEW_REMOVE_SUCCESS: 'Campaign Preview Remove Success',
|
|
169
|
+
CAMPAIGN_PREVIEW_REMOVE_FAILED: 'Campaign Preview Remove Failed',
|
|
166
170
|
|
|
167
171
|
// AI Assistant transcript events
|
|
168
172
|
AI_ASSISTANT_SEND_EVENT_SUCCESS: 'AI Assistant Send Event Success',
|
|
@@ -189,7 +189,7 @@ function parseAgentConfigs(profileData: {
|
|
|
189
189
|
|
|
190
190
|
const finalData = {
|
|
191
191
|
teams: teamData,
|
|
192
|
-
defaultDn: userData.
|
|
192
|
+
defaultDn: userData.deafultDialledNumber,
|
|
193
193
|
forceDefaultDn: tenantData.forceDefaultDn,
|
|
194
194
|
forceDefaultDnForAgent: getDefaultAgentDN(agentProfileData.agentDNValidation),
|
|
195
195
|
regexUS: tenantData.dnDefaultRegex,
|
|
@@ -166,7 +166,7 @@ export const endPointMap = {
|
|
|
166
166
|
) =>
|
|
167
167
|
`organization/${orgId}/v2/auxiliary-code?page=${page}&pageSize=${pageSize}${
|
|
168
168
|
filter && filter.length > 0 ? `&filter=id=in=(${filter})` : ''
|
|
169
|
-
}&attributes=${attributes}`,
|
|
169
|
+
}&attributes=${attributes}&desktopProfileFilter=true`,
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Gets the endpoint for organization info.
|
|
@@ -117,6 +117,10 @@ export const CC_TASK_EVENTS = {
|
|
|
117
117
|
CAMPAIGN_CONTACT_UPDATED: 'CampaignContactUpdated',
|
|
118
118
|
/** Event emitted when accepting a campaign preview contact fails */
|
|
119
119
|
CAMPAIGN_PREVIEW_ACCEPT_FAILED: 'CampaignPreviewAcceptFailed',
|
|
120
|
+
/** Event emitted when skipping a campaign preview contact fails */
|
|
121
|
+
CAMPAIGN_PREVIEW_SKIP_FAILED: 'CampaignPreviewSkipFailed',
|
|
122
|
+
/** Event emitted when removing a campaign preview contact fails */
|
|
123
|
+
CAMPAIGN_PREVIEW_REMOVE_FAILED: 'CampaignPreviewRemoveFailed',
|
|
120
124
|
/** Event emitted when a real-time transcript chunk is received */
|
|
121
125
|
REAL_TIME_TRANSCRIPTION: 'REAL_TIME_TRANSCRIPTION',
|
|
122
126
|
} as const;
|
|
@@ -275,8 +279,9 @@ export type AgentResponse = {
|
|
|
275
279
|
|
|
276
280
|
/**
|
|
277
281
|
* The default dialed number of the agent.
|
|
282
|
+
* Note: The API returns this field as "deafultDialledNumber" (with typo).
|
|
278
283
|
*/
|
|
279
|
-
|
|
284
|
+
deafultDialledNumber?: string;
|
|
280
285
|
};
|
|
281
286
|
|
|
282
287
|
/**
|
package/src/services/core/Err.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type TaskErrorIds =
|
|
|
39
39
|
| {'Service.aqm.task.resumeRecording': Failure}
|
|
40
40
|
| {'Service.aqm.dialer.startOutdial': Failure}
|
|
41
41
|
| {'Service.aqm.dialer.acceptPreviewContact': Failure}
|
|
42
|
+
| {'Service.aqm.dialer.skipPreviewContact': Failure}
|
|
43
|
+
| {'Service.aqm.dialer.removePreviewContact': Failure}
|
|
42
44
|
| {'Service.reqs.generic.failure': {trackingId: string}};
|
|
43
45
|
|
|
44
46
|
export type ReqError =
|
|
@@ -49,34 +49,69 @@ const getAgentActionTypeFromTask = (taskData?: TaskData): 'DIAL_NUMBER' | '' =>
|
|
|
49
49
|
return isDialNumber || isEntryPointVariant ? 'DIAL_NUMBER' : '';
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Strips characters defined in the dial plan entry from the input string.
|
|
54
|
+
*
|
|
55
|
+
* @param input - The dial number to sanitize
|
|
56
|
+
* @param strippedChars - String of characters to remove from the input
|
|
57
|
+
* @returns The sanitized input with specified characters removed
|
|
58
|
+
*/
|
|
59
|
+
export const stripDialPlanChars = (input: string, strippedChars: string): string => {
|
|
60
|
+
if (!strippedChars) {
|
|
61
|
+
return input;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const charsToStrip = new Set(strippedChars.split(''));
|
|
65
|
+
|
|
66
|
+
return input
|
|
67
|
+
.split('')
|
|
68
|
+
.filter((c) => !charsToStrip.has(c))
|
|
69
|
+
.join('');
|
|
70
|
+
};
|
|
54
71
|
|
|
55
72
|
/**
|
|
56
73
|
* Validates a dial number against the provided dial plan regex patterns.
|
|
57
74
|
* A number is valid if it matches at least one regex pattern in the dial plans.
|
|
58
|
-
*
|
|
75
|
+
* Skips validation when no dial plan entries are configured, deferring to the server.
|
|
59
76
|
*
|
|
60
77
|
* @param input - The dial number to validate
|
|
61
78
|
* @param dialPlanEntries - Array of dial plan entries containing regex patterns
|
|
62
|
-
* @returns true if the input matches at least one dial plan regex pattern, false otherwise
|
|
79
|
+
* @returns true if the input matches at least one dial plan regex pattern or no entries are configured, false otherwise
|
|
63
80
|
*/
|
|
64
81
|
export const isValidDialNumber = (
|
|
65
82
|
input: string,
|
|
66
83
|
dialPlanEntries: DialPlan['dialPlanEntity']
|
|
67
84
|
): boolean => {
|
|
85
|
+
if (!input) {
|
|
86
|
+
LoggerProxy.warn('Dial number is empty or undefined.', {
|
|
87
|
+
module: 'Utils',
|
|
88
|
+
method: 'isValidDialNumber',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
if (!dialPlanEntries || dialPlanEntries.length === 0) {
|
|
69
|
-
LoggerProxy.
|
|
95
|
+
LoggerProxy.log(
|
|
96
|
+
'No dial plan entries found. Skipping client-side validation, deferring to server.',
|
|
97
|
+
{module: 'Utils', method: 'isValidDialNumber'}
|
|
98
|
+
);
|
|
70
99
|
|
|
71
|
-
return
|
|
100
|
+
return true;
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
return dialPlanEntries.some((entry) => {
|
|
75
104
|
try {
|
|
105
|
+
const sanitizedInput = stripDialPlanChars(input, entry.strippedChars);
|
|
76
106
|
const regex = new RegExp(entry.regex);
|
|
77
107
|
|
|
78
|
-
return regex.test(
|
|
79
|
-
} catch {
|
|
108
|
+
return regex.test(sanitizedInput);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
LoggerProxy.warn(`Failed to validate dial number against entry "${entry.name}": ${e}`, {
|
|
111
|
+
module: 'Utils',
|
|
112
|
+
method: 'isValidDialNumber',
|
|
113
|
+
});
|
|
114
|
+
|
|
80
115
|
return false;
|
|
81
116
|
}
|
|
82
117
|
});
|
|
@@ -312,14 +312,11 @@ export default class TaskManager extends EventEmitter {
|
|
|
312
312
|
case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
|
|
313
313
|
case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
|
|
314
314
|
case CC_EVENTS.AGENT_INVITE_FAILED: {
|
|
315
|
-
LoggerProxy.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
interactionId: payload.data.interactionId,
|
|
321
|
-
}
|
|
322
|
-
);
|
|
315
|
+
LoggerProxy.info(`Task removal triggered by ${payload.data.type}`, {
|
|
316
|
+
module: TASK_MANAGER_FILE,
|
|
317
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
318
|
+
interactionId: payload.data.interactionId,
|
|
319
|
+
});
|
|
323
320
|
task = this.updateTaskData(task, payload.data);
|
|
324
321
|
|
|
325
322
|
const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
|
|
@@ -343,19 +340,31 @@ export default class TaskManager extends EventEmitter {
|
|
|
343
340
|
break;
|
|
344
341
|
}
|
|
345
342
|
case CC_EVENTS.CONTACT_ENDED:
|
|
346
|
-
// Update task data
|
|
343
|
+
// Update task data.
|
|
347
344
|
if (task) {
|
|
348
|
-
LoggerProxy.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
345
|
+
LoggerProxy.info(`Contact ended for interaction`, {
|
|
346
|
+
module: TASK_MANAGER_FILE,
|
|
347
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
348
|
+
interactionId: payload.data.interactionId,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Campaign preview tasks should never trigger wrapup on ContactEnded —
|
|
352
|
+
// they are terminal cleanup events. For all other tasks, derive
|
|
353
|
+
// wrapUpRequired from agentsPendingWrapUp as before.
|
|
354
|
+
const CAMPAIGN_OUTBOUND_TYPES = [
|
|
355
|
+
'STANDARD_PREVIEW_CAMPAIGN',
|
|
356
|
+
'DIRECT_PREVIEW_CAMPAIGN',
|
|
357
|
+
];
|
|
358
|
+
const isCampaignPreview = CAMPAIGN_OUTBOUND_TYPES.includes(
|
|
359
|
+
task.data?.interaction?.outboundType ?? ''
|
|
355
360
|
);
|
|
361
|
+
const wrapUpRequired = isCampaignPreview
|
|
362
|
+
? false
|
|
363
|
+
: payload.data.agentsPendingWrapUp?.includes(this.agentId) || false;
|
|
364
|
+
|
|
356
365
|
task = this.updateTaskData(task, {
|
|
357
366
|
...payload.data,
|
|
358
|
-
wrapUpRequired
|
|
367
|
+
wrapUpRequired,
|
|
359
368
|
});
|
|
360
369
|
|
|
361
370
|
// Handle cleanup based on whether task should be deleted
|
|
@@ -364,12 +373,83 @@ export default class TaskManager extends EventEmitter {
|
|
|
364
373
|
task?.emit(TASK_EVENTS.TASK_END, task);
|
|
365
374
|
}
|
|
366
375
|
break;
|
|
367
|
-
case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
|
|
368
|
-
// CampaignContactUpdated is a non-terminal event (
|
|
369
|
-
//
|
|
370
|
-
//
|
|
376
|
+
case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED: {
|
|
377
|
+
// CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
|
|
378
|
+
// Update the task data and emit an event so consumers can react to the updated contact.
|
|
379
|
+
// Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
|
|
371
380
|
if (task) {
|
|
372
|
-
|
|
381
|
+
// Carry forward campaign preview fields from existing task data since the updated
|
|
382
|
+
// contact payload may not include them, and reconcileData would delete them.
|
|
383
|
+
const existingCpd = task.data?.interaction?.callProcessingDetails;
|
|
384
|
+
const updatedData = {...payload.data};
|
|
385
|
+
|
|
386
|
+
if (existingCpd) {
|
|
387
|
+
const campaignFields = {
|
|
388
|
+
...(existingCpd.campaignPreviewAutoAction && {
|
|
389
|
+
campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
|
|
390
|
+
}),
|
|
391
|
+
...(existingCpd.campaignPreviewOfferTimeout && {
|
|
392
|
+
campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
|
|
393
|
+
}),
|
|
394
|
+
...(existingCpd.campaignPreviewSkipDisabled && {
|
|
395
|
+
campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
|
|
396
|
+
}),
|
|
397
|
+
...(existingCpd.campaignPreviewRemoveDisabled && {
|
|
398
|
+
campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
|
|
399
|
+
}),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (!updatedData.interaction) {
|
|
403
|
+
updatedData.interaction = {} as typeof updatedData.interaction;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
updatedData.interaction = {
|
|
407
|
+
...updatedData.interaction,
|
|
408
|
+
callProcessingDetails: {
|
|
409
|
+
...campaignFields,
|
|
410
|
+
...(updatedData.interaction.callProcessingDetails || {}),
|
|
411
|
+
} as typeof existingCpd,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
|
|
416
|
+
module: TASK_MANAGER_FILE,
|
|
417
|
+
method: METHODS.REGISTER_TASK_LISTENERS,
|
|
418
|
+
interactionId: payload.data.interactionId,
|
|
419
|
+
data: {
|
|
420
|
+
hasCpd: !!updatedData.interaction?.callProcessingDetails,
|
|
421
|
+
autoAction:
|
|
422
|
+
updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
|
|
423
|
+
skipDisabled:
|
|
424
|
+
updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
|
|
425
|
+
removeDisabled:
|
|
426
|
+
updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
task = this.updateTaskData(task, updatedData);
|
|
431
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
|
|
436
|
+
if (task) {
|
|
437
|
+
// Failure payloads are sparse (no interaction field). Spread existing
|
|
438
|
+
// task data first so reconcileData doesn't delete interaction/cpd.
|
|
439
|
+
task = this.updateTaskData(task, {...task.data, ...payload.data});
|
|
440
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED, task);
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
|
|
444
|
+
if (task) {
|
|
445
|
+
task = this.updateTaskData(task, {...task.data, ...payload.data});
|
|
446
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED, task);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
|
|
450
|
+
if (task) {
|
|
451
|
+
task = this.updateTaskData(task, {...task.data, ...payload.data});
|
|
452
|
+
task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED, task);
|
|
373
453
|
}
|
|
374
454
|
break;
|
|
375
455
|
case CC_EVENTS.CONTACT_MERGED:
|
|
@@ -24,6 +24,8 @@ export const CONFERENCE_EXIT = '/conference/exit';
|
|
|
24
24
|
export const CONFERENCE_TRANSFER = '/conference/transfer';
|
|
25
25
|
export const DIALER_API = '/v1/dialer';
|
|
26
26
|
export const CAMPAIGN_PREVIEW_ACCEPT = '/accept';
|
|
27
|
+
export const CAMPAIGN_PREVIEW_SKIP = '/skip';
|
|
28
|
+
export const CAMPAIGN_PREVIEW_REMOVE = '/remove';
|
|
27
29
|
/** 80-second timeout for accepting preview contact (outbound call setup takes longer than default 20s) */
|
|
28
30
|
export const TIMEOUT_PREVIEW_ACCEPT = 80000;
|
|
29
31
|
export const TASK_MANAGER_FILE = 'taskManager';
|