@webex/contact-center 3.11.0-next.21 → 3.11.0-next.23

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 (54) hide show
  1. package/dist/cc.js +71 -0
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +2 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/behavioral-events.js +13 -0
  6. package/dist/metrics/behavioral-events.js.map +1 -1
  7. package/dist/metrics/constants.js +4 -1
  8. package/dist/metrics/constants.js.map +1 -1
  9. package/dist/services/config/types.js +7 -1
  10. package/dist/services/config/types.js.map +1 -1
  11. package/dist/services/core/Err.js.map +1 -1
  12. package/dist/services/core/aqm-reqs.js +92 -17
  13. package/dist/services/core/aqm-reqs.js.map +1 -1
  14. package/dist/services/task/TaskManager.js +60 -3
  15. package/dist/services/task/TaskManager.js.map +1 -1
  16. package/dist/services/task/TaskUtils.js +16 -3
  17. package/dist/services/task/TaskUtils.js.map +1 -1
  18. package/dist/services/task/constants.js +5 -1
  19. package/dist/services/task/constants.js.map +1 -1
  20. package/dist/services/task/dialer.js +51 -0
  21. package/dist/services/task/dialer.js.map +1 -1
  22. package/dist/services/task/types.js +15 -0
  23. package/dist/services/task/types.js.map +1 -1
  24. package/dist/types/cc.d.ts +31 -1
  25. package/dist/types/constants.d.ts +1 -0
  26. package/dist/types/metrics/constants.d.ts +2 -0
  27. package/dist/types/services/config/types.d.ts +12 -0
  28. package/dist/types/services/core/Err.d.ts +2 -0
  29. package/dist/types/services/core/aqm-reqs.d.ts +49 -0
  30. package/dist/types/services/task/TaskUtils.d.ts +8 -0
  31. package/dist/types/services/task/constants.d.ts +4 -0
  32. package/dist/types/services/task/dialer.d.ts +15 -0
  33. package/dist/types/services/task/types.d.ts +23 -1
  34. package/dist/types.js.map +1 -1
  35. package/dist/webex.js +1 -1
  36. package/package.json +2 -2
  37. package/src/cc.ts +99 -1
  38. package/src/constants.ts +1 -0
  39. package/src/metrics/behavioral-events.ts +14 -0
  40. package/src/metrics/constants.ts +4 -0
  41. package/src/services/config/types.ts +6 -0
  42. package/src/services/core/Err.ts +1 -0
  43. package/src/services/core/aqm-reqs.ts +100 -22
  44. package/src/services/task/TaskManager.ts +75 -3
  45. package/src/services/task/TaskUtils.ts +12 -0
  46. package/src/services/task/constants.ts +4 -0
  47. package/src/services/task/dialer.ts +56 -1
  48. package/src/services/task/types.ts +24 -0
  49. package/src/types.ts +2 -1
  50. package/test/unit/spec/cc.ts +65 -0
  51. package/test/unit/spec/services/task/TaskManager.ts +281 -105
  52. package/test/unit/spec/services/task/dialer.ts +198 -112
  53. package/umd/contact-center.min.js +2 -2
  54. package/umd/contact-center.min.js.map +1 -1
@@ -15,6 +15,7 @@ import {METRIC_EVENT_NAMES} from '../../metrics/constants';
15
15
  import {
16
16
  checkParticipantNotInInteraction,
17
17
  getIsConferenceInProgress,
18
+ isCampaignPreviewReservation,
18
19
  isParticipantInMainInteraction,
19
20
  isPrimary,
20
21
  isSecondaryEpDnAgent,
@@ -81,7 +82,8 @@ export default class TaskManager extends EventEmitter {
81
82
 
82
83
  private handleIncomingWebCall = (call: ICall) => {
83
84
  const currentTask = Object.values(this.taskCollection).find(
84
- (task) => task.data.interaction.mediaType === 'telephony'
85
+ (task) =>
86
+ task.data.interaction.mediaType === 'telephony' && !isCampaignPreviewReservation(task)
85
87
  );
86
88
 
87
89
  if (currentTask) {
@@ -249,8 +251,21 @@ export default class TaskManager extends EventEmitter {
249
251
  }
250
252
  break;
251
253
  case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
252
- task = this.updateTaskData(task, payload.data);
253
- task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
254
+ // When a campaign preview contact is accepted, the assigned event may arrive
255
+ // with a new interactionId while the task is stored under the original
256
+ // reservationInteractionId. Fall back to that key so the task is found.
257
+ if (!task && payload.data.reservationInteractionId) {
258
+ task = this.taskCollection[payload.data.reservationInteractionId];
259
+ if (task) {
260
+ // Re-key the task under the new interaction ID and remove the old entry
261
+ delete this.taskCollection[payload.data.reservationInteractionId];
262
+ this.taskCollection[payload.data.interactionId] = task;
263
+ }
264
+ }
265
+ if (task) {
266
+ task = this.updateTaskData(task, payload.data);
267
+ task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
268
+ }
254
269
  break;
255
270
  case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
256
271
  task = this.updateTaskData(task, {
@@ -262,6 +277,14 @@ export default class TaskManager extends EventEmitter {
262
277
  case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
263
278
  case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
264
279
  case CC_EVENTS.AGENT_INVITE_FAILED: {
280
+ LoggerProxy.warn(
281
+ `[DEBUG-CAMPAIGN-CLEAR] Task removal triggered by ${payload.data.type}, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}`,
282
+ {
283
+ module: TASK_MANAGER_FILE,
284
+ method: METHODS.REGISTER_TASK_LISTENERS,
285
+ interactionId: payload.data.interactionId,
286
+ }
287
+ );
265
288
  task = this.updateTaskData(task, payload.data);
266
289
 
267
290
  const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
@@ -287,6 +310,14 @@ export default class TaskManager extends EventEmitter {
287
310
  case CC_EVENTS.CONTACT_ENDED:
288
311
  // Update task data
289
312
  if (task) {
313
+ LoggerProxy.warn(
314
+ `[DEBUG-CAMPAIGN-CLEAR] CONTACT_ENDED, interactionId=${payload.data.interactionId}, taskType=${task?.data?.type}, state=${task?.data?.interaction?.state}`,
315
+ {
316
+ module: TASK_MANAGER_FILE,
317
+ method: METHODS.REGISTER_TASK_LISTENERS,
318
+ interactionId: payload.data.interactionId,
319
+ }
320
+ );
290
321
  task = this.updateTaskData(task, {
291
322
  ...payload.data,
292
323
  wrapUpRequired: payload.data.agentsPendingWrapUp?.includes(this.agentId) || false,
@@ -298,6 +329,14 @@ export default class TaskManager extends EventEmitter {
298
329
  task?.emit(TASK_EVENTS.TASK_END, task);
299
330
  }
300
331
  break;
332
+ case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
333
+ // CampaignContactUpdated is a non-terminal event (intermediate update during accept).
334
+ // Only update the task data — do NOT remove the task or emit TASK_END.
335
+ // Task cleanup is handled by CONTACT_ENDED or other terminal events.
336
+ if (task) {
337
+ task = this.updateTaskData(task, payload.data);
338
+ }
339
+ break;
301
340
  case CC_EVENTS.CONTACT_MERGED:
302
341
  task = this.handleContactMerged(task, payload.data);
303
342
  break;
@@ -480,6 +519,39 @@ export default class TaskManager extends EventEmitter {
480
519
  task = this.updateTaskData(task, payload.data);
481
520
  task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
482
521
  break;
522
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: {
523
+ // Campaign preview contact offered to agent
524
+ // Create a task in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED
525
+ // after acceptPreviewContact) can find and update it.
526
+ // Emit TASK_CAMPAIGN_PREVIEW_RESERVATION instead of TASK_INCOMING so the call
527
+ // does not ring out to the customer before the agent explicitly accepts the preview contact.
528
+ LoggerProxy.log('Campaign preview reservation received', {
529
+ module: TASK_MANAGER_FILE,
530
+ method: METHODS.REGISTER_TASK_LISTENERS,
531
+ interactionId: payload.data.interactionId,
532
+ });
533
+
534
+ if (!this.taskCollection[payload.data.interactionId]) {
535
+ task = new Task(
536
+ this.contact,
537
+ this.webCallingService,
538
+ {
539
+ ...payload.data,
540
+ wrapUpRequired: false,
541
+ isConferenceInProgress: false,
542
+ isAutoAnswering: false,
543
+ },
544
+ this.wrapupData,
545
+ this.agentId
546
+ );
547
+ this.taskCollection[payload.data.interactionId] = task;
548
+ } else {
549
+ task = this.updateTaskData(task, payload.data);
550
+ }
551
+
552
+ this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
553
+ break;
554
+ }
483
555
  default:
484
556
  break;
485
557
  }
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
2
  import {Interaction, ITask, TaskData, MEDIA_CHANNEL} from './types';
3
+ import {CC_EVENTS} from '../config/types';
3
4
  import {OUTDIAL_DIRECTION, OUTDIAL_MEDIA_TYPE, OUTBOUND_TYPE} from '../../constants';
4
5
  import {LoginOption} from '../../types';
5
6
 
@@ -219,3 +220,14 @@ export const shouldAutoAnswerTask = (
219
220
 
220
221
  return false;
221
222
  };
223
+
224
+ /**
225
+ * Checks if a task is a campaign preview reservation that has not yet been accepted.
226
+ * Campaign preview tasks should not trigger incoming call handling until the agent
227
+ * explicitly accepts the preview contact.
228
+ * @param task - The task to check
229
+ * @returns true if the task is a pending campaign preview reservation, false otherwise
230
+ */
231
+ export const isCampaignPreviewReservation = (task: ITask): boolean => {
232
+ return task?.data?.type === CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION;
233
+ };
@@ -20,6 +20,10 @@ export const END = '/end';
20
20
  export const CONSULT_CONFERENCE = '/consult/conference';
21
21
  export const CONFERENCE_EXIT = '/conference/exit';
22
22
  export const CONFERENCE_TRANSFER = '/conference/transfer';
23
+ export const DIALER_API = '/v1/dialer';
24
+ export const CAMPAIGN_PREVIEW_ACCEPT = '/accept';
25
+ /** 80-second timeout for accepting preview contact (outbound call setup takes longer than default 20s) */
26
+ export const TIMEOUT_PREVIEW_ACCEPT = 80000;
23
27
  export const TASK_MANAGER_FILE = 'taskManager';
24
28
  export const TASK_FILE = 'task';
25
29
 
@@ -1,7 +1,14 @@
1
1
  import {CC_EVENTS} from '../config/types';
2
2
  import {WCC_API_GATEWAY} from '../constants';
3
+ import {HTTP_METHODS} from '../../types';
3
4
  import {createErrDetailsObject as err} from '../core/Utils';
4
- import {TASK_MESSAGE_TYPE, TASK_API} from './constants';
5
+ import {
6
+ TASK_MESSAGE_TYPE,
7
+ TASK_API,
8
+ DIALER_API,
9
+ CAMPAIGN_PREVIEW_ACCEPT,
10
+ TIMEOUT_PREVIEW_ACCEPT,
11
+ } from './constants';
5
12
  import * as Contact from './types';
6
13
  import AqmReqs from '../core/aqm-reqs';
7
14
 
@@ -48,5 +55,53 @@ export default function aqmDialer(aqm: AqmReqs) {
48
55
  errId: 'Service.aqm.dialer.startOutdial',
49
56
  },
50
57
  })),
58
+
59
+ /**
60
+ * Accepts a campaign preview contact, initiating the outbound call.
61
+ *
62
+ * @param {Object} p - Parameters object.
63
+ * @param {Contact.PreviewContactPayload} p.data - Payload containing interactionId and campaignId.
64
+ * @returns {Promise<Contact.AgentContact>} A promise that resolves with agent contact on success.
65
+ *
66
+ * Emits:
67
+ * - `CC_EVENTS.AGENT_CONTACT_ASSIGNED` on success
68
+ * - `CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED` on failure
69
+ * @ignore
70
+ */
71
+ acceptPreviewContact: aqm.req((p: {data: Contact.PreviewContactPayload}) => ({
72
+ url: `${DIALER_API}/campaign/${encodeURIComponent(p.data.campaignId)}/preview-task/${
73
+ p.data.interactionId
74
+ }${CAMPAIGN_PREVIEW_ACCEPT}`,
75
+ host: WCC_API_GATEWAY,
76
+ data: {},
77
+ method: HTTP_METHODS.POST,
78
+ timeout: TIMEOUT_PREVIEW_ACCEPT,
79
+ err,
80
+ notifSuccess: {
81
+ bind: {
82
+ type: TASK_MESSAGE_TYPE,
83
+ data: {
84
+ type: [CC_EVENTS.AGENT_CONTACT_ASSIGNED, CC_EVENTS.CONTACT_ENDED],
85
+ __typeMap: {
86
+ typeField: 'type',
87
+ conditions: {
88
+ [CC_EVENTS.AGENT_CONTACT_ASSIGNED]: {
89
+ reservationInteractionId: p.data.interactionId,
90
+ },
91
+ [CC_EVENTS.CONTACT_ENDED]: {interactionId: p.data.interactionId},
92
+ },
93
+ },
94
+ },
95
+ },
96
+ msg: {} as Contact.AgentContact,
97
+ },
98
+ notifFail: {
99
+ bind: {
100
+ type: TASK_MESSAGE_TYPE,
101
+ data: {type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED, campaignId: p.data.campaignId},
102
+ },
103
+ errId: 'Service.aqm.dialer.acceptPreviewContact',
104
+ },
105
+ })),
51
106
  };
52
107
  }
@@ -542,6 +542,18 @@ export enum TASK_EVENTS {
542
542
  * ```
543
543
  */
544
544
  TASK_POST_CALL_ACTIVITY = 'task:postCallActivity',
545
+
546
+ /**
547
+ * Triggered when a campaign preview contact is offered to the agent
548
+ * @example
549
+ * ```typescript
550
+ * task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (data: AgentContact) => {
551
+ * console.log('Campaign preview contact received:', data.interactionId);
552
+ * // Handle campaign preview reservation
553
+ * });
554
+ * ```
555
+ */
556
+ TASK_CAMPAIGN_PREVIEW_RESERVATION = 'task:campaignPreviewReservation',
545
557
  }
546
558
 
547
559
  /**
@@ -1117,6 +1129,18 @@ export type DialerPayload = {
1117
1129
  origin: string;
1118
1130
  };
1119
1131
 
1132
+ /**
1133
+ * Payload for campaign preview contact operations (accept, skip, remove)
1134
+ * @public
1135
+ */
1136
+ export type PreviewContactPayload = {
1137
+ /** The interaction ID from the campaign reservation */
1138
+ interactionId: string;
1139
+ /** The campaign name (not a UUID). Available from the reservation event at
1140
+ * `task.data.interaction.callProcessingDetails.campaignId` or `task.data.campaignId`. */
1141
+ campaignId: string;
1142
+ };
1143
+
1120
1144
  /**
1121
1145
  * Data structure for cleaning up contact resources
1122
1146
  * @public
package/src/types.ts CHANGED
@@ -541,7 +541,8 @@ export type RequestBody =
541
541
  | Contact.ConsultTransferPayLoad
542
542
  | Contact.cancelCtq
543
543
  | Contact.WrapupPayLoad
544
- | Contact.DialerPayload;
544
+ | Contact.DialerPayload
545
+ | Contact.PreviewContactPayload;
545
546
 
546
547
  /**
547
548
  * Represents the options to fetch buddy agents for the logged in agent.
@@ -129,6 +129,7 @@ describe('webex.cc', () => {
129
129
 
130
130
  dialer: {
131
131
  startOutdial: jest.fn(),
132
+ acceptPreviewContact: jest.fn(),
132
133
  },
133
134
  };
134
135
 
@@ -2180,4 +2181,68 @@ describe('webex.cc', () => {
2180
2181
  );
2181
2182
  });
2182
2183
  });
2184
+
2185
+ describe('acceptPreviewContact', () => {
2186
+ const previewPayload = {
2187
+ interactionId: 'interaction-123',
2188
+ campaignId: 'campaign-456',
2189
+ };
2190
+
2191
+ it('should accept preview contact successfully', async () => {
2192
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
2193
+
2194
+ const acceptPreviewContactMock = jest
2195
+ .spyOn(webex.cc.services.dialer, 'acceptPreviewContact')
2196
+ .mockResolvedValue(mockResponse);
2197
+
2198
+ const result = await webex.cc.acceptPreviewContact(previewPayload);
2199
+
2200
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
2201
+ module: CC_FILE,
2202
+ method: 'acceptPreviewContact',
2203
+ });
2204
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
2205
+ 'Campaign preview contact accepted successfully',
2206
+ {
2207
+ module: CC_FILE,
2208
+ method: 'acceptPreviewContact',
2209
+ trackingId: 'track-123',
2210
+ interactionId: previewPayload.interactionId,
2211
+ }
2212
+ );
2213
+
2214
+ expect(acceptPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
2215
+ expect(result).toEqual(mockResponse);
2216
+ });
2217
+
2218
+ it('should handle error during acceptPreviewContact', async () => {
2219
+ getErrorDetailsSpy.mockRestore();
2220
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
2221
+
2222
+ const error = {
2223
+ details: {
2224
+ trackingId: '1234',
2225
+ data: {
2226
+ reason: 'Error while performing acceptPreviewContact',
2227
+ },
2228
+ },
2229
+ };
2230
+
2231
+ jest.spyOn(webex.cc.services.dialer, 'acceptPreviewContact').mockRejectedValue(error);
2232
+
2233
+ await expect(webex.cc.acceptPreviewContact(previewPayload)).rejects.toThrow(
2234
+ error.details.data.reason
2235
+ );
2236
+
2237
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
2238
+ module: CC_FILE,
2239
+ method: 'acceptPreviewContact',
2240
+ });
2241
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
2242
+ `acceptPreviewContact failed with reason: ${error.details.data.reason}`,
2243
+ {module: CC_FILE, method: 'acceptPreviewContact', trackingId: error.details.trackingId}
2244
+ );
2245
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2246
+ });
2247
+ });
2183
2248
  });