@webex/contact-center 3.12.0-task-refactor.11 → 3.12.0-task-refactor.13

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 (101) hide show
  1. package/dist/cc.js +297 -83
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +3 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/behavioral-events.js +39 -0
  6. package/dist/metrics/behavioral-events.js.map +1 -1
  7. package/dist/metrics/constants.js +8 -1
  8. package/dist/metrics/constants.js.map +1 -1
  9. package/dist/services/ApiAiAssistant.js +3 -0
  10. package/dist/services/ApiAiAssistant.js.map +1 -1
  11. package/dist/services/config/Util.js +1 -1
  12. package/dist/services/config/Util.js.map +1 -1
  13. package/dist/services/config/types.js +10 -0
  14. package/dist/services/config/types.js.map +1 -1
  15. package/dist/services/constants.js +1 -0
  16. package/dist/services/constants.js.map +1 -1
  17. package/dist/services/core/Err.js.map +1 -1
  18. package/dist/services/core/Utils.js +57 -6
  19. package/dist/services/core/Utils.js.map +1 -1
  20. package/dist/services/core/aqm-reqs.js +92 -17
  21. package/dist/services/core/aqm-reqs.js.map +1 -1
  22. package/dist/services/core/websocket/WebSocketManager.js +24 -3
  23. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  24. package/dist/services/index.js +1 -0
  25. package/dist/services/index.js.map +1 -1
  26. package/dist/services/task/Task.js +6 -0
  27. package/dist/services/task/Task.js.map +1 -1
  28. package/dist/services/task/TaskManager.js +155 -6
  29. package/dist/services/task/TaskManager.js.map +1 -1
  30. package/dist/services/task/TaskUtils.js +39 -11
  31. package/dist/services/task/TaskUtils.js.map +1 -1
  32. package/dist/services/task/constants.js +7 -1
  33. package/dist/services/task/constants.js.map +1 -1
  34. package/dist/services/task/dialer.js +129 -0
  35. package/dist/services/task/dialer.js.map +1 -1
  36. package/dist/services/task/state-machine/TaskStateMachine.js +30 -3
  37. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  38. package/dist/services/task/state-machine/actions.js +4 -0
  39. package/dist/services/task/state-machine/actions.js.map +1 -1
  40. package/dist/services/task/state-machine/constants.js +3 -0
  41. package/dist/services/task/state-machine/constants.js.map +1 -1
  42. package/dist/services/task/state-machine/guards.js +23 -0
  43. package/dist/services/task/state-machine/guards.js.map +1 -1
  44. package/dist/services/task/state-machine/types.js.map +1 -1
  45. package/dist/services/task/types.js +59 -0
  46. package/dist/services/task/types.js.map +1 -1
  47. package/dist/types/cc.d.ts +76 -2
  48. package/dist/types/constants.d.ts +3 -0
  49. package/dist/types/metrics/constants.d.ts +6 -0
  50. package/dist/types/services/config/types.d.ts +22 -1
  51. package/dist/types/services/core/Err.d.ts +6 -0
  52. package/dist/types/services/core/Utils.d.ts +20 -2
  53. package/dist/types/services/core/aqm-reqs.d.ts +49 -0
  54. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  55. package/dist/types/services/core/websocket/connection-service.d.ts +1 -0
  56. package/dist/types/services/task/Task.d.ts +1 -0
  57. package/dist/types/services/task/TaskUtils.d.ts +13 -0
  58. package/dist/types/services/task/constants.d.ts +6 -0
  59. package/dist/types/services/task/dialer.d.ts +45 -0
  60. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +36 -8
  61. package/dist/types/services/task/state-machine/constants.d.ts +3 -0
  62. package/dist/types/services/task/state-machine/guards.d.ts +2 -0
  63. package/dist/types/services/task/state-machine/types.d.ts +11 -0
  64. package/dist/types/services/task/types.d.ts +75 -1
  65. package/dist/types.js.map +1 -1
  66. package/dist/webex.js +1 -1
  67. package/package.json +2 -2
  68. package/src/cc.ts +389 -111
  69. package/src/constants.ts +3 -0
  70. package/src/metrics/behavioral-events.ts +42 -0
  71. package/src/metrics/constants.ts +9 -1
  72. package/src/services/ApiAiAssistant.ts +3 -0
  73. package/src/services/config/Util.ts +1 -1
  74. package/src/services/config/types.ts +12 -1
  75. package/src/services/constants.ts +1 -0
  76. package/src/services/core/Err.ts +3 -0
  77. package/src/services/core/Utils.ts +68 -5
  78. package/src/services/core/aqm-reqs.ts +100 -22
  79. package/src/services/core/websocket/WebSocketManager.ts +23 -2
  80. package/src/services/index.ts +1 -0
  81. package/src/services/task/Task.ts +13 -0
  82. package/src/services/task/TaskManager.ts +171 -7
  83. package/src/services/task/TaskUtils.ts +33 -1
  84. package/src/services/task/constants.ts +6 -0
  85. package/src/services/task/dialer.ts +136 -1
  86. package/src/services/task/state-machine/TaskStateMachine.ts +37 -5
  87. package/src/services/task/state-machine/actions.ts +4 -0
  88. package/src/services/task/state-machine/constants.ts +3 -0
  89. package/src/services/task/state-machine/guards.ts +30 -0
  90. package/src/services/task/state-machine/types.ts +14 -1
  91. package/src/services/task/types.ts +80 -0
  92. package/src/types.ts +4 -1
  93. package/test/unit/spec/cc.ts +294 -32
  94. package/test/unit/spec/services/config/index.ts +30 -2
  95. package/test/unit/spec/services/core/Utils.ts +367 -1
  96. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +91 -1
  97. package/test/unit/spec/services/core/websocket/connection-service.ts +4 -5
  98. package/test/unit/spec/services/task/TaskManager.ts +4 -2
  99. package/test/unit/spec/services/task/dialer.ts +372 -96
  100. package/umd/contact-center.min.js +2 -2
  101. package/umd/contact-center.min.js.map +1 -1
@@ -19,7 +19,13 @@ import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
19
19
  import {CC_EVENTS, WrapupData} from '../config/types';
20
20
  import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
21
21
  import LoggerProxy from '../../logger-proxy';
22
- import {getIsConferenceInProgress, isSecondaryEpDnAgent, shouldAutoAnswerTask} from './TaskUtils';
22
+ import {
23
+ getIsConferenceInProgress,
24
+ isCampaignPreviewTask,
25
+ isCampaignPreviewReservation,
26
+ isSecondaryEpDnAgent,
27
+ shouldAutoAnswerTask,
28
+ } from './TaskUtils';
23
29
  import TaskFactory from './TaskFactory';
24
30
  import WebRTC from './voice/WebRTC';
25
31
  import {TaskEvent, type TaskEventPayload} from './state-machine';
@@ -144,7 +150,8 @@ export default class TaskManager extends EventEmitter {
144
150
 
145
151
  private handleIncomingWebCall = (call: ICall) => {
146
152
  const currentTask = Object.values(this.taskCollection).find(
147
- (t) => t.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY
153
+ (task) =>
154
+ task.data.interaction.mediaType === 'telephony' && !isCampaignPreviewReservation(task)
148
155
  );
149
156
 
150
157
  if (currentTask) {
@@ -194,8 +201,23 @@ export default class TaskManager extends EventEmitter {
194
201
  switch (ccEvent) {
195
202
  // CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
196
203
  case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
204
+ if (isCampaignPreviewTask(payload)) {
205
+ return {
206
+ type: TaskEvent.TASK_INCOMING,
207
+ taskData: payload,
208
+ isCampaignReservationAccept: true,
209
+ };
210
+ }
211
+
197
212
  return {type: TaskEvent.TASK_INCOMING, taskData: payload};
198
213
 
214
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: // -> TASK_INCOMING (campaign branch via guard)
215
+ return {
216
+ type: TaskEvent.TASK_INCOMING,
217
+ taskData: payload,
218
+ isCampaignReservationAccept: true,
219
+ };
220
+
199
221
  case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
200
222
  return {type: TaskEvent.TASK_OFFERED, taskData: payload};
201
223
 
@@ -282,7 +304,9 @@ export default class TaskManager extends EventEmitter {
282
304
  type: TaskEvent.CONTACT_ENDED,
283
305
  taskData: {
284
306
  ...payload,
285
- wrapUpRequired: payload.interaction?.state !== 'new',
307
+ wrapUpRequired: isCampaignPreviewTask(payload)
308
+ ? false
309
+ : payload.agentsPendingWrapUp?.includes(agentId || '') || false,
286
310
  },
287
311
  };
288
312
 
@@ -298,6 +322,15 @@ export default class TaskManager extends EventEmitter {
298
322
  case CC_EVENTS.AGENT_OUTBOUND_FAILED:
299
323
  return {type: TaskEvent.OUTBOUND_FAILED, taskData: payload, reason: payload.reason};
300
324
 
325
+ case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
326
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
327
+
328
+ case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
329
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED, taskData: payload};
330
+
331
+ case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
332
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED, taskData: payload};
333
+
301
334
  case CC_EVENTS.CONTACT_RECORDING_STARTED:
302
335
  return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
303
336
 
@@ -434,7 +467,20 @@ export default class TaskManager extends EventEmitter {
434
467
  }
435
468
 
436
469
  const interactionId = message.data.interactionId;
437
- const task = this.taskCollection[interactionId];
470
+ const reservationInteractionId = message.data.reservationInteractionId;
471
+ let task = this.taskCollection[interactionId];
472
+
473
+ // When a campaign preview contact is accepted, the assigned event may arrive
474
+ // with a new interactionId while the task is stored under the original
475
+ // reservationInteractionId. Fall back to that key so the task is found.
476
+ if (!task && reservationInteractionId) {
477
+ task = this.taskCollection[reservationInteractionId];
478
+ if (task) {
479
+ // Re-key the task under the new interaction ID and remove the old entry
480
+ delete this.taskCollection[reservationInteractionId];
481
+ this.taskCollection[interactionId] = task;
482
+ }
483
+ }
438
484
 
439
485
  const wasConsultedTask = Boolean(task?.data?.isConsulted);
440
486
  const computeWrapUpRequired = () => {
@@ -503,11 +549,115 @@ export default class TaskManager extends EventEmitter {
503
549
  case CC_EVENTS.CONTACT_MERGED:
504
550
  return this.handleContactMergedEvent(context);
505
551
 
552
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
553
+ return this.handleCampaignPreviewReservation(context);
554
+
555
+ case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
556
+ return this.handleCampaignContactUpdated(context);
557
+
506
558
  default:
507
559
  return {task: context.task};
508
560
  }
509
561
  }
510
562
 
563
+ private handleCampaignContactUpdated(context: EventContext) {
564
+ const {payload} = context;
565
+ let {task} = context;
566
+ // CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
567
+ // Update the task data and emit an event so consumers can react to the updated contact.
568
+ // Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
569
+ if (task) {
570
+ // Carry forward campaign preview fields from existing task data since the updated
571
+ // contact payload may not include them, and reconcileData would delete them.
572
+ const existingCpd = task.data?.interaction?.callProcessingDetails;
573
+ const updatedData: TaskData = {...payload};
574
+
575
+ if (existingCpd) {
576
+ const campaignFields = {
577
+ ...(existingCpd.campaignPreviewAutoAction && {
578
+ campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
579
+ }),
580
+ ...(existingCpd.campaignPreviewOfferTimeout && {
581
+ campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
582
+ }),
583
+ ...(existingCpd.campaignPreviewSkipDisabled && {
584
+ campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
585
+ }),
586
+ ...(existingCpd.campaignPreviewRemoveDisabled && {
587
+ campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
588
+ }),
589
+ };
590
+
591
+ if (!updatedData.interaction) {
592
+ updatedData.interaction = {} as typeof updatedData.interaction;
593
+ }
594
+
595
+ updatedData.interaction = {
596
+ ...updatedData.interaction,
597
+ callProcessingDetails: {
598
+ ...campaignFields,
599
+ ...(updatedData.interaction.callProcessingDetails || {}),
600
+ } as typeof existingCpd,
601
+ };
602
+ }
603
+
604
+ LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
605
+ module: TASK_MANAGER_FILE,
606
+ method: METHODS.REGISTER_TASK_LISTENERS,
607
+ interactionId: payload.interactionId,
608
+ data: {
609
+ hasCpd: !!updatedData.interaction?.callProcessingDetails,
610
+ autoAction: updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
611
+ skipDisabled: updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
612
+ removeDisabled:
613
+ updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
614
+ },
615
+ });
616
+
617
+ task = this.updateTaskData(task, updatedData);
618
+ task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
619
+ }
620
+
621
+ return {task};
622
+ }
623
+
624
+ /**
625
+ * Creates or updates a task for campaign preview reservation.
626
+ * TASK_CAMPAIGN_PREVIEW_RESERVATION is emitted by the state machine (campaign TASK_INCOMING branch).
627
+ */
628
+ private handleCampaignPreviewReservation(context: EventContext): TaskEventActions {
629
+ const {payload} = context;
630
+ let {task} = context;
631
+
632
+ LoggerProxy.log('Campaign preview reservation received', {
633
+ module: TASK_MANAGER_FILE,
634
+ method: METHODS.REGISTER_TASK_LISTENERS,
635
+ interactionId: payload.interactionId,
636
+ });
637
+
638
+ if (!task) {
639
+ task = TaskFactory.createTask(
640
+ this.contact,
641
+ this.webCallingService,
642
+ {
643
+ ...payload,
644
+ wrapUpRequired: false,
645
+ isConferenceInProgress: false,
646
+ isAutoAnswering: false,
647
+ },
648
+ this.configFlags,
649
+ this.wrapupData,
650
+ this.agentId
651
+ );
652
+ this.setupTaskListeners(task);
653
+ this.taskCollection[payload.interactionId] = task;
654
+ } else {
655
+ task = this.updateTaskData(task, payload);
656
+ }
657
+
658
+ return {task};
659
+ }
660
+
511
661
  /**
512
662
  * Handle AGENT_CONTACT_RESERVED event
513
663
  * Creates a new task; state machine event is sent during processing
@@ -624,6 +774,16 @@ export default class TaskManager extends EventEmitter {
624
774
  this.emit(TASK_EVENTS.TASK_INCOMING, t);
625
775
  });
626
776
 
777
+ task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (t: ITask) => {
778
+ LoggerProxy.log(`Campaign preview reservation event received`, {
779
+ module: TASK_MANAGER_FILE,
780
+ method: METHODS.REGISTER_TASK_LISTENERS,
781
+ interactionId: t.data?.interactionId,
782
+ });
783
+
784
+ this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, t);
785
+ });
786
+
627
787
  // Listen for TASK_HYDRATE on the task and re-emit on TaskManager
628
788
  task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
629
789
  // Task data is already updated by the task itself before emitting
@@ -735,6 +895,7 @@ export default class TaskManager extends EventEmitter {
735
895
  * @private
736
896
  */
737
897
  private handleTaskCleanup(task: ITask) {
898
+ // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
738
899
  if (
739
900
  this.webCallingService.loginOption === LoginOption.BROWSER &&
740
901
  task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
@@ -746,13 +907,16 @@ export default class TaskManager extends EventEmitter {
746
907
 
747
908
  const isOutdial = task.data.interaction.outboundType === 'OUTDIAL';
748
909
  const isNew = task.data.interaction.state === 'new';
749
- const needsWrapUp = task.data.agentsPendingWrapUp?.length > 0;
910
+ const needsWrapUp = task.data.agentsPendingWrapUp?.includes(this.agentId) ?? false;
750
911
 
751
912
  // For OUTDIAL: only remove if NOT terminated (user-declined, no wrap-up follows)
752
- // If terminated, keep task for wrap-up flow (CONTACT_ENDED → AGENT_WRAPUP)
753
913
  // For non-OUTDIAL: remove if state is 'new'
754
914
  // Always remove if secondary EpDn agent
755
- if ((isNew && !(isOutdial && needsWrapUp)) || isSecondaryEpDnAgent(task.data.interaction)) {
915
+ if (
916
+ (isNew && !(isOutdial && needsWrapUp)) ||
917
+ isSecondaryEpDnAgent(task.data.interaction) ||
918
+ (!needsWrapUp && isOutdial) // For outdial tasks, needs wrap-up is false and state is "WRAPUP". We need to just remove the task.
919
+ ) {
756
920
  this.removeTaskFromCollection(task);
757
921
  }
758
922
  }
@@ -1,9 +1,13 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
2
  import {Interaction, ITask, TaskData, MEDIA_CHANNEL} from './types';
3
- import {OUTDIAL_DIRECTION, OUTDIAL_MEDIA_TYPE, OUTBOUND_TYPE} from '../../constants';
4
3
  import {LoginOption} from '../../types';
5
4
  import {PARTICIPANT_TYPE, MEDIA_TYPE_MAIN_CALL} from './state-machine/constants';
6
5
  import {TaskContext} from './state-machine/types';
6
+ import {CC_EVENTS} from '../config/types';
7
+ import {OUTBOUND_TYPE, OUTDIAL_DIRECTION, OUTDIAL_MEDIA_TYPE} from '../../constants';
8
+
9
+ const CAMPAIGN_PREVIEW_OUTBOUND_TYPES = ['STANDARD_PREVIEW_CAMPAIGN', 'DIRECT_PREVIEW_CAMPAIGN'];
10
+ const CAMPAIGN_PREVIEW_CAMPAIGN_TYPES = ['preview_standard', 'preview_direct'];
7
11
 
8
12
  /**
9
13
  * Checks if the customer is still in the call (not left)
@@ -240,6 +244,23 @@ export const isSecondaryEpDnAgent = (interaction: Interaction): boolean => {
240
244
  return interaction.mediaType === 'telephony' && isSecondaryAgent(interaction);
241
245
  };
242
246
 
247
+ /**
248
+ * Checks if the task belongs to a campaign preview interaction.
249
+ * Campaign preview ContactEnded events are terminal cleanup events and should not trigger wrapup.
250
+ */
251
+ export const isCampaignPreviewTask = (taskData?: TaskData | null): boolean => {
252
+ const outboundType = taskData?.interaction?.outboundType ?? '';
253
+ const cpd = taskData?.interaction?.callProcessingDetails as unknown as
254
+ | Record<string, string | undefined>
255
+ | undefined;
256
+ const campaignType = cpd?.campaignType ?? '';
257
+
258
+ return (
259
+ CAMPAIGN_PREVIEW_OUTBOUND_TYPES.includes(outboundType) ||
260
+ CAMPAIGN_PREVIEW_CAMPAIGN_TYPES.includes(campaignType)
261
+ );
262
+ };
263
+
243
264
  /**
244
265
  * Checks if auto-answer is enabled for the agent participant
245
266
  * @param interaction - The interaction object
@@ -379,3 +400,14 @@ export const getConsultMediaResourceId = (
379
400
 
380
401
  return undefined;
381
402
  };
403
+
404
+ /**
405
+ * Checks if a task is a campaign preview reservation that has not yet been accepted.
406
+ * Campaign preview tasks should not trigger incoming call handling until the agent
407
+ * explicitly accepts the preview contact.
408
+ * @param task - The task to check
409
+ * @returns true if the task is a pending campaign preview reservation, false otherwise
410
+ */
411
+ export const isCampaignPreviewReservation = (task: ITask): boolean => {
412
+ return task?.data?.type === CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION;
413
+ };
@@ -22,6 +22,12 @@ export const END = '/end';
22
22
  export const CONSULT_CONFERENCE = '/consult/conference';
23
23
  export const CONFERENCE_EXIT = '/conference/exit';
24
24
  export const CONFERENCE_TRANSFER = '/conference/transfer';
25
+ export const DIALER_API = '/v1/dialer';
26
+ export const CAMPAIGN_PREVIEW_ACCEPT = '/accept';
27
+ export const CAMPAIGN_PREVIEW_SKIP = '/skip';
28
+ export const CAMPAIGN_PREVIEW_REMOVE = '/remove';
29
+ /** 80-second timeout for accepting preview contact (outbound call setup takes longer than default 20s) */
30
+ export const TIMEOUT_PREVIEW_ACCEPT = 80000;
25
31
  export const TASK_MANAGER_FILE = 'taskManager';
26
32
  export const TASK_FILE = 'task';
27
33
 
@@ -1,7 +1,16 @@
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
+ CAMPAIGN_PREVIEW_SKIP,
11
+ CAMPAIGN_PREVIEW_REMOVE,
12
+ TIMEOUT_PREVIEW_ACCEPT,
13
+ } from './constants';
5
14
  import * as Contact from './types';
6
15
  import AqmReqs from '../core/aqm-reqs';
7
16
 
@@ -48,5 +57,131 @@ export default function aqmDialer(aqm: AqmReqs) {
48
57
  errId: 'Service.aqm.dialer.startOutdial',
49
58
  },
50
59
  })),
60
+
61
+ /**
62
+ * Accepts a campaign preview contact, initiating the outbound call.
63
+ *
64
+ * @param {Object} p - Parameters object.
65
+ * @param {Contact.PreviewContactPayload} p.data - Payload containing interactionId and campaignId.
66
+ * @returns {Promise<Contact.AgentContact>} A promise that resolves with agent contact on success.
67
+ *
68
+ * Emits:
69
+ * - `CC_EVENTS.AGENT_CONTACT_ASSIGNED` on success
70
+ * - `CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED` on failure
71
+ * @ignore
72
+ */
73
+ acceptPreviewContact: aqm.req((p: {data: Contact.PreviewContactPayload}) => ({
74
+ url: `${DIALER_API}/campaign/${encodeURIComponent(p.data.campaignId)}/preview-task/${
75
+ p.data.interactionId
76
+ }${CAMPAIGN_PREVIEW_ACCEPT}`,
77
+ host: WCC_API_GATEWAY,
78
+ data: {},
79
+ method: HTTP_METHODS.POST,
80
+ timeout: TIMEOUT_PREVIEW_ACCEPT,
81
+ err,
82
+ notifSuccess: {
83
+ bind: {
84
+ type: TASK_MESSAGE_TYPE,
85
+ data: {
86
+ type: [CC_EVENTS.AGENT_CONTACT_ASSIGNED, CC_EVENTS.CONTACT_ENDED],
87
+ __typeMap: {
88
+ typeField: 'type',
89
+ conditions: {
90
+ [CC_EVENTS.AGENT_CONTACT_ASSIGNED]: {
91
+ reservationInteractionId: p.data.interactionId,
92
+ },
93
+ [CC_EVENTS.CONTACT_ENDED]: {interactionId: p.data.interactionId},
94
+ },
95
+ },
96
+ },
97
+ },
98
+ msg: {} as Contact.AgentContact,
99
+ },
100
+ notifFail: {
101
+ bind: {
102
+ type: TASK_MESSAGE_TYPE,
103
+ data: {type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED, campaignId: p.data.campaignId},
104
+ },
105
+ errId: 'Service.aqm.dialer.acceptPreviewContact',
106
+ },
107
+ })),
108
+
109
+ /**
110
+ * Skips a campaign preview contact, requesting the next contact from the campaign.
111
+ *
112
+ * @param {Object} p - Parameters object.
113
+ * @param {Contact.PreviewContactPayload} p.data - Payload containing interactionId and campaignId.
114
+ * @returns {Promise<Contact.AgentContact>} A promise that resolves with agent contact on success.
115
+ *
116
+ * Emits:
117
+ * - `CC_EVENTS.CAMPAIGN_CONTACT_UPDATED` or `CC_EVENTS.CONTACT_ENDED` on success
118
+ * - `CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED` on failure
119
+ * @ignore
120
+ */
121
+ skipPreviewContact: aqm.req((p: {data: Contact.PreviewContactPayload}) => ({
122
+ url: `${DIALER_API}/campaign/${encodeURIComponent(p.data.campaignId)}/preview-task/${
123
+ p.data.interactionId
124
+ }${CAMPAIGN_PREVIEW_SKIP}`,
125
+ host: WCC_API_GATEWAY,
126
+ data: {},
127
+ method: HTTP_METHODS.POST,
128
+ err,
129
+ notifSuccess: {
130
+ bind: {
131
+ type: TASK_MESSAGE_TYPE,
132
+ data: {
133
+ type: [CC_EVENTS.CAMPAIGN_CONTACT_UPDATED, CC_EVENTS.CONTACT_ENDED],
134
+ interactionId: p.data.interactionId,
135
+ },
136
+ },
137
+ msg: {} as Contact.AgentContact,
138
+ },
139
+ notifFail: {
140
+ bind: {
141
+ type: TASK_MESSAGE_TYPE,
142
+ data: {type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED, campaignId: p.data.campaignId},
143
+ },
144
+ errId: 'Service.aqm.dialer.skipPreviewContact',
145
+ },
146
+ })),
147
+
148
+ /**
149
+ * Removes a campaign preview contact from the campaign list entirely.
150
+ *
151
+ * @param {Object} p - Parameters object.
152
+ * @param {Contact.PreviewContactPayload} p.data - Payload containing interactionId and campaignId.
153
+ * @returns {Promise<Contact.AgentContact>} A promise that resolves with agent contact on success.
154
+ *
155
+ * Emits:
156
+ * - `CC_EVENTS.CAMPAIGN_CONTACT_UPDATED` or `CC_EVENTS.CONTACT_ENDED` on success
157
+ * - `CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED` on failure
158
+ * @ignore
159
+ */
160
+ removePreviewContact: aqm.req((p: {data: Contact.PreviewContactPayload}) => ({
161
+ url: `${DIALER_API}/campaign/${encodeURIComponent(p.data.campaignId)}/preview-task/${
162
+ p.data.interactionId
163
+ }${CAMPAIGN_PREVIEW_REMOVE}`,
164
+ host: WCC_API_GATEWAY,
165
+ data: {},
166
+ method: HTTP_METHODS.POST,
167
+ err,
168
+ notifSuccess: {
169
+ bind: {
170
+ type: TASK_MESSAGE_TYPE,
171
+ data: {
172
+ type: [CC_EVENTS.CAMPAIGN_CONTACT_UPDATED, CC_EVENTS.CONTACT_ENDED],
173
+ interactionId: p.data.interactionId,
174
+ },
175
+ },
176
+ msg: {} as Contact.AgentContact,
177
+ },
178
+ notifFail: {
179
+ bind: {
180
+ type: TASK_MESSAGE_TYPE,
181
+ data: {type: CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED, campaignId: p.data.campaignId},
182
+ },
183
+ errId: 'Service.aqm.dialer.removePreviewContact',
184
+ },
185
+ })),
51
186
  };
52
187
  }
@@ -126,11 +126,18 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
126
126
  actions: ['updateTaskData', 'emitTaskHydrate'],
127
127
  },
128
128
  ],
129
- // AgentContactReserved (applicable for direct incoming/consult/transfer/outdial)
130
- [TaskEvent.TASK_INCOMING]: {
131
- target: TaskState.OFFERED,
132
- actions: ['initializeTask', 'emitTaskIncoming'],
133
- },
129
+ // AgentContactReserved (direct incoming/consult/transfer/outdial, or campaign preview accept)
130
+ [TaskEvent.TASK_INCOMING]: [
131
+ {
132
+ guard: guards.isCampaignReservationAccept,
133
+ target: TaskState.OFFERED,
134
+ actions: ['initializeTask', 'emitTaskCampaignPreviewReservation'],
135
+ },
136
+ {
137
+ target: TaskState.OFFERED,
138
+ actions: ['initializeTask', 'emitTaskIncoming'],
139
+ },
140
+ ],
134
141
 
135
142
  // AgentOutboundFailed can arrive before TASK_INCOMING due to race conditions
136
143
  [TaskEvent.OUTBOUND_FAILED]: {
@@ -208,6 +215,15 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
208
215
  actions: ['updateTaskData', 'markEnded', 'emitTaskOutdialFailed', 'emitTaskEnd'],
209
216
  },
210
217
  ],
218
+ [TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED]: {
219
+ actions: ['updateTaskData', 'emitTaskCampaignPreviewAcceptFailed'],
220
+ },
221
+ [TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED]: {
222
+ actions: ['updateTaskData', 'emitTaskCampaignPreviewSkipFailed'],
223
+ },
224
+ [TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED]: {
225
+ actions: ['updateTaskData', 'emitTaskCampaignPreviewRemoveFailed'],
226
+ },
211
227
  // AgentConsulting comes for received after the initial consult is accepted
212
228
  [TaskEvent.CONSULTING_ACTIVE]: [
213
229
  {
@@ -266,6 +282,10 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
266
282
  ],
267
283
  },
268
284
  // AgentContactAssigned can be resent after consult transfers; keep context in sync
285
+ /* TODO: This transition needs to be checked if this is even needed as receiver will
286
+ * be in Consult Accept state which can be consulting state and ASSIGNED will be only
287
+ * for the receiver. So receuving ASSIGNED in Connected state is highly unlikely.
288
+ */
269
289
  [TaskEvent.ASSIGN]: {
270
290
  target: TaskState.CONNECTED,
271
291
  actions: ['updateTaskData', 'emitTaskAssigned'],
@@ -319,6 +339,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
319
339
  ],
320
340
  // AgentContactEnded Event
321
341
  [TaskEvent.CONTACT_ENDED]: [
342
+ {
343
+ // Campaign preview ContactEnded is terminal cleanup; never enter wrapup.
344
+ guard: guards.isCampaignPreviewContactEnded,
345
+ target: TaskState.TERMINATED,
346
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
347
+ },
322
348
  {
323
349
  // Conference still active → CONFERENCING
324
350
  guard: guards.conferenceInProgressFromEvent,
@@ -426,6 +452,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
426
452
  actions: ['updateTaskData'],
427
453
  },
428
454
  [TaskEvent.CONTACT_ENDED]: [
455
+ {
456
+ // Campaign preview ContactEnded is terminal cleanup; never enter wrapup.
457
+ guard: guards.isCampaignPreviewContactEnded,
458
+ target: TaskState.TERMINATED,
459
+ actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
460
+ },
429
461
  {
430
462
  guard: guards.conferenceInProgressFromEvent,
431
463
  target: TaskState.CONFERENCING,
@@ -651,6 +651,10 @@ export const actions: TaskActionsMap = {
651
651
  cleanupResources: () => undefined,
652
652
 
653
653
  // Event emitters - placeholders overridden by consumers
654
+ emitTaskCampaignPreviewReservation: () => undefined,
655
+ emitTaskCampaignPreviewAcceptFailed: () => undefined,
656
+ emitTaskCampaignPreviewSkipFailed: () => undefined,
657
+ emitTaskCampaignPreviewRemoveFailed: () => undefined,
654
658
  emitTaskIncoming: () => undefined,
655
659
  emitTaskHydrate: () => undefined,
656
660
  emitTaskOfferContact: () => undefined,
@@ -153,6 +153,9 @@ export enum TaskEvent {
153
153
  ASSIGN_FAILED = 'ASSIGN_FAILED',
154
154
  INVITE_FAILED = 'INVITE_FAILED',
155
155
  OUTBOUND_FAILED = 'OUTBOUND_FAILED',
156
+ CAMPAIGN_PREVIEW_ACCEPT_FAILED = 'CAMPAIGN_PREVIEW_ACCEPT_FAILED',
157
+ CAMPAIGN_PREVIEW_SKIP_FAILED = 'CAMPAIGN_PREVIEW_SKIP_FAILED',
158
+ CAMPAIGN_PREVIEW_REMOVE_FAILED = 'CAMPAIGN_PREVIEW_REMOVE_FAILED',
156
159
 
157
160
  // Switch events (toggle between consult and main call)
158
161
  SWITCH_TO_MAIN_CALL = 'SWITCH_TO_MAIN_CALL',
@@ -26,6 +26,7 @@ import {
26
26
  getIsCustomerInCall,
27
27
  getConferenceParticipantsCount,
28
28
  getIsConferenceInProgress,
29
+ isCampaignPreviewTask,
29
30
  } from '../TaskUtils';
30
31
  import {TaskEvent, INTERACTION_STATE, CONSULT_STATE, MEDIA_TYPE_CONSULT} from './constants';
31
32
 
@@ -296,6 +297,31 @@ export const guards = {
296
297
  },
297
298
 
298
299
  // Wrapup Guards
300
+ isCampaignPreviewContactEnded: ({context, event}: GuardParams): boolean => {
301
+ if (event?.type !== TaskEvent.CONTACT_ENDED) {
302
+ return false;
303
+ }
304
+
305
+ const taskData = getTaskDataFromEvent(event);
306
+ if (!isCampaignPreviewTask(taskData)) {
307
+ return false;
308
+ }
309
+
310
+ const selfAgentId = getSelfAgentId(context, taskData);
311
+ const pending = taskData?.agentsPendingWrapUp;
312
+ const isPendingWrapUp =
313
+ Array.isArray(pending) && !!selfAgentId && pending.includes(selfAgentId);
314
+ const participantIsWrapUp =
315
+ !!selfAgentId && taskData?.interaction?.participants?.[selfAgentId]?.isWrapUp === true;
316
+ const explicitWrapUp =
317
+ taskData?.wrapUpRequired === true ||
318
+ participantIsWrapUp ||
319
+ isPendingWrapUp ||
320
+ taskData?.interaction?.state === 'wrapUp';
321
+
322
+ return !explicitWrapUp;
323
+ },
324
+
299
325
  shouldWrapUp: ({context, event}: GuardParams): boolean => {
300
326
  const taskData = getTaskDataFromEvent(event);
301
327
  if (!taskData) return false;
@@ -362,6 +388,10 @@ export const guards = {
362
388
  return false;
363
389
  },
364
390
 
391
+ isCampaignReservationAccept: ({event}: GuardParams): boolean => {
392
+ return event?.type === TaskEvent.TASK_INCOMING && event.isCampaignReservationAccept === true;
393
+ },
394
+
365
395
  // Server State Guards
366
396
  isPrimaryMediaOnHold: ({event}: GuardParams): boolean => {
367
397
  const taskData = getTaskDataFromEvent(event);
@@ -84,7 +84,11 @@ type BaseEvent<T extends TaskEvent> = {type: T};
84
84
  * Event payload mapping - defines the payload for each event type
85
85
  */
86
86
  interface TaskEventPayloadMap {
87
- [TaskEvent.TASK_INCOMING]: BaseEvent<TaskEvent.TASK_INCOMING> & {taskData: TaskData};
87
+ [TaskEvent.TASK_INCOMING]: BaseEvent<TaskEvent.TASK_INCOMING> & {
88
+ taskData: TaskData;
89
+ /** Set when campaign preview reservation accept is receieved */
90
+ isCampaignReservationAccept?: boolean;
91
+ };
88
92
  [TaskEvent.TASK_OFFERED]: BaseEvent<TaskEvent.TASK_OFFERED> & {taskData: TaskData};
89
93
  [TaskEvent.OFFER_CONSULT]: BaseEvent<TaskEvent.OFFER_CONSULT> & {taskData: TaskData};
90
94
  [TaskEvent.HYDRATE]: BaseEvent<TaskEvent.HYDRATE> & {taskData: TaskData; agentId?: string};
@@ -173,6 +177,15 @@ interface TaskEventPayloadMap {
173
177
  reason?: string;
174
178
  taskData?: TaskData;
175
179
  };
180
+ [TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED]: BaseEvent<TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED> & {
181
+ taskData?: TaskData;
182
+ };
183
+ [TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED]: BaseEvent<TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED> & {
184
+ taskData?: TaskData;
185
+ };
186
+ [TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED]: BaseEvent<TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED> & {
187
+ taskData?: TaskData;
188
+ };
176
189
  [TaskEvent.SWITCH_TO_MAIN_CALL]: BaseEvent<TaskEvent.SWITCH_TO_MAIN_CALL>;
177
190
  [TaskEvent.SWITCH_TO_CONSULT]: BaseEvent<TaskEvent.SWITCH_TO_CONSULT>;
178
191
  [TaskEvent.ACCEPT]: BaseEvent<TaskEvent.ACCEPT>;