@webex/contact-center 3.12.0-next.6 → 3.12.0-next.61

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.
Files changed (60) hide show
  1. package/dist/cc.js +161 -21
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/behavioral-events.js +26 -0
  6. package/dist/metrics/behavioral-events.js.map +1 -1
  7. package/dist/metrics/constants.js +4 -0
  8. package/dist/metrics/constants.js.map +1 -1
  9. package/dist/services/config/Util.js +1 -1
  10. package/dist/services/config/Util.js.map +1 -1
  11. package/dist/services/config/constants.js +1 -1
  12. package/dist/services/config/constants.js.map +1 -1
  13. package/dist/services/config/types.js +4 -0
  14. package/dist/services/config/types.js.map +1 -1
  15. package/dist/services/core/Err.js.map +1 -1
  16. package/dist/services/core/Utils.js +37 -9
  17. package/dist/services/core/Utils.js.map +1 -1
  18. package/dist/services/task/TaskManager.js +90 -8
  19. package/dist/services/task/TaskManager.js.map +1 -1
  20. package/dist/services/task/constants.js +3 -1
  21. package/dist/services/task/constants.js.map +1 -1
  22. package/dist/services/task/dialer.js +78 -0
  23. package/dist/services/task/dialer.js.map +1 -1
  24. package/dist/services/task/index.js +7 -2
  25. package/dist/services/task/index.js.map +1 -1
  26. package/dist/services/task/types.js +44 -0
  27. package/dist/services/task/types.js.map +1 -1
  28. package/dist/types/cc.d.ts +44 -0
  29. package/dist/types/constants.d.ts +2 -0
  30. package/dist/types/metrics/constants.d.ts +4 -0
  31. package/dist/types/services/config/types.d.ts +10 -1
  32. package/dist/types/services/core/Err.d.ts +4 -0
  33. package/dist/types/services/core/Utils.d.ts +10 -3
  34. package/dist/types/services/task/constants.d.ts +2 -0
  35. package/dist/types/services/task/dialer.d.ts +30 -0
  36. package/dist/types/services/task/types.d.ts +53 -1
  37. package/dist/webex.js +1 -1
  38. package/package.json +9 -9
  39. package/src/cc.ts +196 -22
  40. package/src/constants.ts +2 -0
  41. package/src/metrics/behavioral-events.ts +28 -0
  42. package/src/metrics/constants.ts +4 -0
  43. package/src/services/config/Util.ts +1 -1
  44. package/src/services/config/constants.ts +1 -1
  45. package/src/services/config/types.ts +6 -1
  46. package/src/services/core/Err.ts +2 -0
  47. package/src/services/core/Utils.ts +43 -8
  48. package/src/services/task/TaskManager.ts +102 -22
  49. package/src/services/task/constants.ts +2 -0
  50. package/src/services/task/dialer.ts +80 -0
  51. package/src/services/task/index.ts +7 -2
  52. package/src/services/task/types.ts +56 -0
  53. package/test/unit/spec/cc.ts +154 -20
  54. package/test/unit/spec/services/config/index.ts +3 -3
  55. package/test/unit/spec/services/core/Utils.ts +90 -7
  56. package/test/unit/spec/services/task/TaskManager.ts +238 -7
  57. package/test/unit/spec/services/task/dialer.ts +190 -0
  58. package/test/unit/spec/services/task/index.ts +21 -0
  59. package/umd/contact-center.min.js +2 -2
  60. 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.info('Starting CC SDK registration', {
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.info('Connecting to websocket', {
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.info('Connecting to RTD websocket', {
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
- LoggerProxy.info('Starting agent station login', {
849
- module: CC_FILE,
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
- data.loginOption === LoginOption.AGENT_DN &&
861
- !isValidDialNumber(data.dialNumber, dialPlanEntries)
862
- ) {
863
- const error = new Error('INVALID_DIAL_NUMBER');
864
- // @ts-ignore - adding custom key to the error object
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
- throw error;
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.info('event=handleConnectionLost | Connection lost', {
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.info(
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.info('Starting silent relogin process', {
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.info(
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
  /**
@@ -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.defaultDialledNumber,
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
- defaultDialledNumber?: string;
284
+ deafultDialledNumber?: string;
280
285
  };
281
286
 
282
287
  /**
@@ -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
- // Fallback regex for US/Canada dial numbers when no dial plan entries are configured
53
- export const FALLBACK_DIAL_NUMBER_REGEX = /1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}/;
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
- * Falls back to US/Canada regex validation if no dial plan entries are configured.
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.info('No dial plan entries found. Falling back to US number validation.');
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 FALLBACK_DIAL_NUMBER_REGEX.test(input);
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(input);
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.warn(
316
- `[DEBUG-CAMPAIGN-CLEAR] Task removal triggered by ${payload.data.type}, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}`,
317
- {
318
- module: TASK_MANAGER_FILE,
319
- method: METHODS.REGISTER_TASK_LISTENERS,
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.warn(
349
- `[DEBUG-CAMPAIGN-CLEAR] CONTACT_ENDED, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}, state=${task?.data?.interaction?.state}`,
350
- {
351
- module: TASK_MANAGER_FILE,
352
- method: METHODS.REGISTER_TASK_LISTENERS,
353
- interactionId: payload.data.interactionId,
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: payload.data.agentsPendingWrapUp?.includes(this.agentId) || false,
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 (intermediate update during accept).
369
- // Only update the task data do NOT remove the task or emit TASK_END.
370
- // Task cleanup is handled by CONTACT_ENDED or other terminal events.
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
- task = this.updateTaskData(task, payload.data);
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';