@webex/contact-center 3.12.0-task-refactor.11 → 3.12.0-task-refactor.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cc.js +297 -83
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -1
- package/dist/metrics/behavioral-events.js +39 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +8 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +3 -0
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/Util.js +1 -1
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/types.js +10 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/constants.js +1 -0
- package/dist/services/constants.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/Utils.js +57 -6
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +92 -17
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/websocket/WebSocketManager.js +24 -3
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/task/Task.js +6 -0
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskManager.js +155 -6
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +39 -11
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +7 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +129 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +30 -3
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +4 -0
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/constants.js +3 -0
- package/dist/services/task/state-machine/constants.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +23 -0
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/types.js.map +1 -1
- package/dist/services/task/types.js +59 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/types/cc.d.ts +76 -2
- package/dist/types/constants.d.ts +3 -0
- package/dist/types/metrics/constants.d.ts +6 -0
- package/dist/types/services/config/types.d.ts +22 -1
- package/dist/types/services/core/Err.d.ts +6 -0
- package/dist/types/services/core/Utils.d.ts +20 -2
- package/dist/types/services/core/aqm-reqs.d.ts +49 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
- package/dist/types/services/core/websocket/connection-service.d.ts +1 -0
- package/dist/types/services/task/Task.d.ts +1 -0
- package/dist/types/services/task/TaskUtils.d.ts +13 -0
- package/dist/types/services/task/constants.d.ts +6 -0
- package/dist/types/services/task/dialer.d.ts +45 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +36 -8
- package/dist/types/services/task/state-machine/constants.d.ts +3 -0
- package/dist/types/services/task/state-machine/guards.d.ts +2 -0
- package/dist/types/services/task/state-machine/types.d.ts +11 -0
- package/dist/types/services/task/types.d.ts +75 -1
- package/dist/types.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/cc.ts +389 -111
- package/src/constants.ts +3 -0
- package/src/metrics/behavioral-events.ts +42 -0
- package/src/metrics/constants.ts +9 -1
- package/src/services/ApiAiAssistant.ts +3 -0
- package/src/services/config/Util.ts +1 -1
- package/src/services/config/types.ts +12 -1
- package/src/services/constants.ts +1 -0
- package/src/services/core/Err.ts +3 -0
- package/src/services/core/Utils.ts +68 -5
- package/src/services/core/aqm-reqs.ts +100 -22
- package/src/services/core/websocket/WebSocketManager.ts +23 -2
- package/src/services/index.ts +1 -0
- package/src/services/task/Task.ts +13 -0
- package/src/services/task/TaskManager.ts +171 -7
- package/src/services/task/TaskUtils.ts +33 -1
- package/src/services/task/constants.ts +6 -0
- package/src/services/task/dialer.ts +136 -1
- package/src/services/task/state-machine/TaskStateMachine.ts +37 -5
- package/src/services/task/state-machine/actions.ts +4 -0
- package/src/services/task/state-machine/constants.ts +3 -0
- package/src/services/task/state-machine/guards.ts +30 -0
- package/src/services/task/state-machine/types.ts +14 -1
- package/src/services/task/types.ts +80 -0
- package/src/types.ts +4 -1
- package/test/unit/spec/cc.ts +293 -26
- package/test/unit/spec/services/config/index.ts +30 -2
- package/test/unit/spec/services/core/Utils.ts +367 -1
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +90 -1
- package/test/unit/spec/services/core/websocket/connection-service.ts +4 -5
- package/test/unit/spec/services/task/TaskManager.ts +2325 -150
- package/test/unit/spec/services/task/dialer.ts +372 -96
- package/umd/contact-center.min.js +2 -2
- 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 {
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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?.
|
|
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 (
|
|
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 {
|
|
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 (
|
|
130
|
-
[TaskEvent.TASK_INCOMING]:
|
|
131
|
-
|
|
132
|
-
|
|
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> & {
|
|
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>;
|