@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.
- package/dist/cc.js +71 -0
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/metrics/behavioral-events.js +13 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/config/types.js +7 -1
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.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/task/TaskManager.js +60 -3
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +16 -3
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +5 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/dialer.js +51 -0
- package/dist/services/task/dialer.js.map +1 -1
- package/dist/services/task/types.js +15 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/types/cc.d.ts +31 -1
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +2 -0
- package/dist/types/services/config/types.d.ts +12 -0
- package/dist/types/services/core/Err.d.ts +2 -0
- package/dist/types/services/core/aqm-reqs.d.ts +49 -0
- package/dist/types/services/task/TaskUtils.d.ts +8 -0
- package/dist/types/services/task/constants.d.ts +4 -0
- package/dist/types/services/task/dialer.d.ts +15 -0
- package/dist/types/services/task/types.d.ts +23 -1
- package/dist/types.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/cc.ts +99 -1
- package/src/constants.ts +1 -0
- package/src/metrics/behavioral-events.ts +14 -0
- package/src/metrics/constants.ts +4 -0
- package/src/services/config/types.ts +6 -0
- package/src/services/core/Err.ts +1 -0
- package/src/services/core/aqm-reqs.ts +100 -22
- package/src/services/task/TaskManager.ts +75 -3
- package/src/services/task/TaskUtils.ts +12 -0
- package/src/services/task/constants.ts +4 -0
- package/src/services/task/dialer.ts +56 -1
- package/src/services/task/types.ts +24 -0
- package/src/types.ts +2 -1
- package/test/unit/spec/cc.ts +65 -0
- package/test/unit/spec/services/task/TaskManager.ts +281 -105
- package/test/unit/spec/services/task/dialer.ts +198 -112
- package/umd/contact-center.min.js +2 -2
- 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) =>
|
|
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
|
-
|
|
253
|
-
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 {
|
|
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.
|
package/test/unit/spec/cc.ts
CHANGED
|
@@ -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
|
});
|