@webex/contact-center 3.12.0-next.8 → 3.12.0-task-refactor.1

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 (200) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +65 -123
  24. package/dist/cc.js.map +1 -1
  25. package/dist/constants.js +13 -2
  26. package/dist/constants.js.map +1 -1
  27. package/dist/index.js +13 -5
  28. package/dist/index.js.map +1 -1
  29. package/dist/metrics/behavioral-events.js +26 -13
  30. package/dist/metrics/behavioral-events.js.map +1 -1
  31. package/dist/metrics/constants.js +7 -6
  32. package/dist/metrics/constants.js.map +1 -1
  33. package/dist/services/ApiAiAssistant.js +0 -3
  34. package/dist/services/ApiAiAssistant.js.map +1 -1
  35. package/dist/services/config/Util.js +2 -3
  36. package/dist/services/config/Util.js.map +1 -1
  37. package/dist/services/config/types.js +16 -14
  38. package/dist/services/config/types.js.map +1 -1
  39. package/dist/services/constants.js +0 -1
  40. package/dist/services/constants.js.map +1 -1
  41. package/dist/services/core/Err.js.map +1 -1
  42. package/dist/services/core/Utils.js +79 -55
  43. package/dist/services/core/Utils.js.map +1 -1
  44. package/dist/services/core/aqm-reqs.js +17 -92
  45. package/dist/services/core/aqm-reqs.js.map +1 -1
  46. package/dist/services/core/websocket/WebSocketManager.js +5 -25
  47. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  48. package/dist/services/core/websocket/types.js.map +1 -1
  49. package/dist/services/index.js +1 -2
  50. package/dist/services/index.js.map +1 -1
  51. package/dist/services/task/Task.js +644 -0
  52. package/dist/services/task/Task.js.map +1 -0
  53. package/dist/services/task/TaskFactory.js +45 -0
  54. package/dist/services/task/TaskFactory.js.map +1 -0
  55. package/dist/services/task/TaskManager.js +556 -532
  56. package/dist/services/task/TaskManager.js.map +1 -1
  57. package/dist/services/task/TaskUtils.js +132 -28
  58. package/dist/services/task/TaskUtils.js.map +1 -1
  59. package/dist/services/task/constants.js +7 -6
  60. package/dist/services/task/constants.js.map +1 -1
  61. package/dist/services/task/dialer.js +0 -51
  62. package/dist/services/task/dialer.js.map +1 -1
  63. package/dist/services/task/digital/Digital.js +77 -0
  64. package/dist/services/task/digital/Digital.js.map +1 -0
  65. package/dist/services/task/state-machine/TaskStateMachine.js +634 -0
  66. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  67. package/dist/services/task/state-machine/actions.js +366 -0
  68. package/dist/services/task/state-machine/actions.js.map +1 -0
  69. package/dist/services/task/state-machine/constants.js +139 -0
  70. package/dist/services/task/state-machine/constants.js.map +1 -0
  71. package/dist/services/task/state-machine/guards.js +256 -0
  72. package/dist/services/task/state-machine/guards.js.map +1 -0
  73. package/dist/services/task/state-machine/index.js +53 -0
  74. package/dist/services/task/state-machine/index.js.map +1 -0
  75. package/dist/services/task/state-machine/types.js +54 -0
  76. package/dist/services/task/state-machine/types.js.map +1 -0
  77. package/dist/services/task/state-machine/uiControlsComputer.js +369 -0
  78. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  79. package/dist/services/task/taskDataNormalizer.js +99 -0
  80. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  81. package/dist/services/task/types.js +157 -18
  82. package/dist/services/task/types.js.map +1 -1
  83. package/dist/services/task/voice/Voice.js +1031 -0
  84. package/dist/services/task/voice/Voice.js.map +1 -0
  85. package/dist/services/task/voice/WebRTC.js +149 -0
  86. package/dist/services/task/voice/WebRTC.js.map +1 -0
  87. package/dist/types/cc.d.ts +4 -33
  88. package/dist/types/constants.d.ts +13 -2
  89. package/dist/types/index.d.ts +11 -5
  90. package/dist/types/metrics/constants.d.ts +5 -3
  91. package/dist/types/services/ApiAiAssistant.d.ts +1 -1
  92. package/dist/types/services/config/types.d.ts +97 -25
  93. package/dist/types/services/core/Err.d.ts +0 -2
  94. package/dist/types/services/core/Utils.d.ts +25 -23
  95. package/dist/types/services/core/aqm-reqs.d.ts +0 -49
  96. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -1
  97. package/dist/types/services/core/websocket/connection-service.d.ts +0 -1
  98. package/dist/types/services/core/websocket/types.d.ts +1 -1
  99. package/dist/types/services/index.d.ts +1 -1
  100. package/dist/types/services/task/Task.d.ts +146 -0
  101. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  102. package/dist/types/services/task/TaskUtils.d.ts +39 -8
  103. package/dist/types/services/task/constants.d.ts +5 -4
  104. package/dist/types/services/task/dialer.d.ts +0 -15
  105. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  106. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +906 -0
  107. package/dist/types/services/task/state-machine/actions.d.ts +8 -0
  108. package/dist/types/services/task/state-machine/constants.d.ts +91 -0
  109. package/dist/types/services/task/state-machine/guards.d.ts +78 -0
  110. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  111. package/dist/types/services/task/state-machine/types.d.ts +256 -0
  112. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  113. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  114. package/dist/types/services/task/types.d.ts +539 -88
  115. package/dist/types/services/task/voice/Voice.d.ts +183 -0
  116. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  117. package/dist/types/types.d.ts +68 -0
  118. package/dist/types/webex.d.ts +1 -0
  119. package/dist/types.js +70 -0
  120. package/dist/types.js.map +1 -1
  121. package/dist/webex.js +14 -2
  122. package/dist/webex.js.map +1 -1
  123. package/package.json +14 -11
  124. package/src/cc.ts +91 -177
  125. package/src/constants.ts +13 -2
  126. package/src/index.ts +14 -5
  127. package/src/metrics/ai-docs/AGENTS.md +348 -0
  128. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  129. package/src/metrics/behavioral-events.ts +28 -14
  130. package/src/metrics/constants.ts +7 -8
  131. package/src/services/ApiAiAssistant.ts +2 -4
  132. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  133. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  134. package/src/services/ai-docs/AGENTS.md +384 -0
  135. package/src/services/config/Util.ts +2 -3
  136. package/src/services/config/ai-docs/AGENTS.md +253 -0
  137. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  138. package/src/services/config/types.ts +108 -20
  139. package/src/services/constants.ts +0 -1
  140. package/src/services/core/Err.ts +0 -1
  141. package/src/services/core/Utils.ts +90 -67
  142. package/src/services/core/ai-docs/AGENTS.md +379 -0
  143. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  144. package/src/services/core/aqm-reqs.ts +22 -100
  145. package/src/services/core/websocket/WebSocketManager.ts +4 -23
  146. package/src/services/core/websocket/types.ts +1 -1
  147. package/src/services/index.ts +1 -2
  148. package/src/services/task/Task.ts +785 -0
  149. package/src/services/task/TaskFactory.ts +55 -0
  150. package/src/services/task/TaskManager.ts +567 -633
  151. package/src/services/task/TaskUtils.ts +175 -31
  152. package/src/services/task/ai-docs/AGENTS.md +448 -0
  153. package/src/services/task/ai-docs/ARCHITECTURE.md +573 -0
  154. package/src/services/task/constants.ts +5 -4
  155. package/src/services/task/dialer.ts +1 -56
  156. package/src/services/task/digital/Digital.ts +95 -0
  157. package/src/services/task/state-machine/TaskStateMachine.ts +793 -0
  158. package/src/services/task/state-machine/actions.ts +409 -0
  159. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  160. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  161. package/src/services/task/state-machine/constants.ts +150 -0
  162. package/src/services/task/state-machine/guards.ts +295 -0
  163. package/src/services/task/state-machine/index.ts +28 -0
  164. package/src/services/task/state-machine/types.ts +228 -0
  165. package/src/services/task/state-machine/uiControlsComputer.ts +529 -0
  166. package/src/services/task/taskDataNormalizer.ts +137 -0
  167. package/src/services/task/types.ts +641 -95
  168. package/src/services/task/voice/Voice.ts +1255 -0
  169. package/src/services/task/voice/WebRTC.ts +187 -0
  170. package/src/types.ts +88 -5
  171. package/src/utils/AGENTS.md +276 -0
  172. package/src/webex.js +2 -0
  173. package/test/unit/spec/cc.ts +59 -142
  174. package/test/unit/spec/logger-proxy.ts +70 -0
  175. package/test/unit/spec/services/ApiAiAssistant.ts +17 -0
  176. package/test/unit/spec/services/config/index.ts +26 -55
  177. package/test/unit/spec/services/core/Utils.ts +103 -52
  178. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +48 -112
  179. package/test/unit/spec/services/core/websocket/connection-service.ts +5 -4
  180. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  181. package/test/unit/spec/services/task/Task.ts +416 -0
  182. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  183. package/test/unit/spec/services/task/TaskManager.ts +781 -1735
  184. package/test/unit/spec/services/task/TaskUtils.ts +125 -0
  185. package/test/unit/spec/services/task/dialer.ts +112 -198
  186. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  187. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +473 -0
  188. package/test/unit/spec/services/task/state-machine/guards.ts +288 -0
  189. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  190. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +147 -0
  191. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  192. package/test/unit/spec/services/task/voice/Voice.ts +587 -0
  193. package/test/unit/spec/services/task/voice/WebRTC.ts +242 -0
  194. package/umd/contact-center.min.js +2 -2
  195. package/umd/contact-center.min.js.map +1 -1
  196. package/dist/services/task/index.js +0 -1525
  197. package/dist/services/task/index.js.map +0 -1
  198. package/dist/types/services/task/index.d.ts +0 -650
  199. package/src/services/task/index.ts +0 -1801
  200. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,785 @@
1
+ import {EventEmitter} from 'events';
2
+ import {createActor} from 'xstate';
3
+ import type {ActorRefFrom, SnapshotFrom} from 'xstate';
4
+ import {
5
+ ITask,
6
+ TaskData,
7
+ TaskResponse,
8
+ WrapupPayLoad,
9
+ TaskId,
10
+ TransferPayLoad,
11
+ DESTINATION_TYPE,
12
+ TASK_EVENTS,
13
+ TaskUIControls,
14
+ ConsultEndPayload,
15
+ ConsultPayload,
16
+ ConsultTransferPayLoad,
17
+ ResumeRecordingPayload,
18
+ MEDIA_CHANNEL,
19
+ TASK_CHANNEL_TYPE,
20
+ VOICE_VARIANT,
21
+ CallId,
22
+ } from './types';
23
+ import {METHODS} from './constants';
24
+ import {CC_FILE, TASK_FILE} from '../../constants';
25
+ import {getErrorDetails} from '../core/Utils';
26
+ import routingContact from './contact';
27
+ import MetricsManager from '../../metrics/MetricsManager';
28
+ import {METRIC_EVENT_NAMES} from '../../metrics/constants';
29
+ import LoggerProxy from '../../logger-proxy';
30
+ import {createTaskStateMachine, TaskState} from './state-machine';
31
+ import type {
32
+ TaskEventPayload,
33
+ TaskStateMachine,
34
+ UIControlConfig,
35
+ TaskContext,
36
+ TaskActionsMap,
37
+ TaskActionArgs,
38
+ } from './state-machine';
39
+ import {
40
+ computeUIControls,
41
+ getDefaultUIControls,
42
+ haveUIControlsChanged,
43
+ } from './state-machine/uiControlsComputer';
44
+ import AutoWrapup from './AutoWrapup';
45
+ import {WrapupData} from '../config/types';
46
+
47
+ type UIControlConfigInput = Omit<UIControlConfig, 'channelType'> & {
48
+ channelType?: UIControlConfig['channelType'];
49
+ };
50
+
51
+ export default abstract class Task extends EventEmitter implements ITask {
52
+ protected contact: ReturnType<typeof routingContact>;
53
+ protected metricsManager: MetricsManager;
54
+ public stateMachineService?: ActorRefFrom<TaskStateMachine>;
55
+ public data: TaskData;
56
+ public webCallMap: Record<TaskId, CallId>;
57
+ public state?: SnapshotFrom<TaskStateMachine>;
58
+ private lastState?: TaskState;
59
+ protected currentUiControls: TaskUIControls;
60
+ protected uiControlConfig: UIControlConfig;
61
+ protected wrapupData?: WrapupData;
62
+ public autoWrapup?: AutoWrapup;
63
+ protected agentId?: string;
64
+
65
+ constructor(
66
+ contact: ReturnType<typeof routingContact>,
67
+ data: TaskData,
68
+ uiControlConfig: UIControlConfigInput,
69
+ wrapupData?: WrapupData,
70
+ agentId?: string
71
+ ) {
72
+ super();
73
+ this.contact = contact;
74
+ this.data = data;
75
+ const channelType = uiControlConfig.channelType ?? Task.resolveChannelType(data);
76
+ // Include agentId in the config for ownership checks (transfer conference)
77
+ this.uiControlConfig = {...uiControlConfig, channelType, agentId};
78
+ this.wrapupData = wrapupData;
79
+ this.agentId = agentId;
80
+ this.metricsManager = MetricsManager.getInstance();
81
+ this.webCallMap = {};
82
+ this.currentUiControls = getDefaultUIControls();
83
+ this.initializeStateMachine();
84
+ this.setupAutoWrapupTimer();
85
+ }
86
+
87
+ private static resolveChannelType(data: TaskData): UIControlConfig['channelType'] {
88
+ const mediaType = data?.interaction?.mediaType ?? MEDIA_CHANNEL.TELEPHONY;
89
+
90
+ return mediaType === MEDIA_CHANNEL.TELEPHONY
91
+ ? TASK_CHANNEL_TYPE.VOICE
92
+ : TASK_CHANNEL_TYPE.DIGITAL;
93
+ }
94
+
95
+ // Abstract methods that all child classes must implement
96
+ public abstract accept(): Promise<TaskResponse>;
97
+
98
+ // Voice-specific methods with default implementations that throw errors
99
+ // Voice class will override these with actual implementations
100
+ public async decline(): Promise<TaskResponse> {
101
+ this.unsupportedMethodError('decline');
102
+ }
103
+
104
+ public async pauseRecording(): Promise<TaskResponse> {
105
+ this.unsupportedMethodError('pauseRecording');
106
+ }
107
+
108
+ public async resumeRecording(
109
+ resumeRecordingPayload: ResumeRecordingPayload
110
+ ): Promise<TaskResponse> {
111
+ if (resumeRecordingPayload) {
112
+ // parameter intentionally unused
113
+ }
114
+ this.unsupportedMethodError('resumeRecording');
115
+ }
116
+
117
+ public async consult(consultPayload: ConsultPayload): Promise<TaskResponse> {
118
+ if (consultPayload) {
119
+ // parameter intentionally unused
120
+ }
121
+ this.unsupportedMethodError('consult');
122
+ }
123
+
124
+ public async endConsult(consultEndPayload: ConsultEndPayload): Promise<TaskResponse> {
125
+ if (consultEndPayload) {
126
+ // parameter intentionally unused
127
+ }
128
+ this.unsupportedMethodError('endConsult');
129
+ }
130
+
131
+ public async consultTransfer(
132
+ consultTransferPayload?: ConsultTransferPayLoad
133
+ ): Promise<TaskResponse> {
134
+ if (consultTransferPayload) {
135
+ // parameter intentionally unused
136
+ }
137
+ this.unsupportedMethodError('consultTransfer');
138
+ }
139
+
140
+ public async consultConference(): Promise<TaskResponse> {
141
+ this.unsupportedMethodError('consultConference');
142
+ }
143
+
144
+ public async exitConference(): Promise<TaskResponse> {
145
+ this.unsupportedMethodError('exitConference');
146
+ }
147
+
148
+ public async transferConference(): Promise<TaskResponse> {
149
+ this.unsupportedMethodError('transferConference');
150
+ }
151
+
152
+ public async switchCall(): Promise<TaskResponse> {
153
+ this.unsupportedMethodError('switchCall');
154
+ }
155
+
156
+ public async toggleMute(): Promise<void> {
157
+ this.unsupportedMethodError('toggleMute');
158
+ }
159
+
160
+ public unregisterWebCallListeners(): void {
161
+ // Default implementation - child classes can override
162
+ LoggerProxy.log('unregisterWebCallListeners called', {
163
+ module: CC_FILE,
164
+ method: 'unregisterWebCallListeners',
165
+ interactionId: this.data?.interactionId,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Cancel any in-progress auto wrap-up timer.
171
+ * Base implementation just clears the timer reference so subclasses inherit the behavior.
172
+ */
173
+ public cancelAutoWrapupTimer(): void {
174
+ if (this.autoWrapup) {
175
+ this.autoWrapup.clear();
176
+ this.autoWrapup = undefined;
177
+ LoggerProxy.log('Auto wrap-up timer cancelled', {
178
+ module: CC_FILE,
179
+ method: 'cancelAutoWrapupTimer',
180
+ interactionId: this.data?.interactionId,
181
+ });
182
+ }
183
+ }
184
+
185
+ // Voice tasks use holdResume(), but provide separate methods for interface compliance
186
+ public async hold(): Promise<TaskResponse> {
187
+ this.unsupportedMethodError('hold');
188
+ }
189
+
190
+ public async resume(): Promise<TaskResponse> {
191
+ this.unsupportedMethodError('resume');
192
+ }
193
+
194
+ public async holdResume(): Promise<TaskResponse> {
195
+ this.unsupportedMethodError('holdResume');
196
+ }
197
+
198
+ /**
199
+ * Latest UI controls derived from state machine state and context.
200
+ */
201
+ public get uiControls(): TaskUIControls {
202
+ return this.currentUiControls;
203
+ }
204
+
205
+ protected updateUiControls(forceEmit = false): void {
206
+ const nextControls = this.computeUIControls();
207
+ const shouldEmit = forceEmit || haveUIControlsChanged(this.currentUiControls, nextControls);
208
+ this.currentUiControls = nextControls;
209
+
210
+ if (shouldEmit) {
211
+ this.emit(TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, this.currentUiControls);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Initialize the state machine
217
+ */
218
+ private initializeStateMachine(): void {
219
+ const machine: TaskStateMachine = createTaskStateMachine(this.uiControlConfig, {
220
+ actions: this.getStateMachineActionOverrides(),
221
+ });
222
+
223
+ this.stateMachineService = createActor(machine);
224
+
225
+ this.stateMachineService.subscribe((snapshot) => {
226
+ const previousState = this.lastState;
227
+ const currentState = snapshot.value as TaskState;
228
+ LoggerProxy.log(`State machine transition: ${previousState || 'N/A'} -> ${currentState}`, {
229
+ module: CC_FILE,
230
+ method: 'onTransition',
231
+ // @ts-ignore - snapshot may include event detail depending on XState version
232
+ eventType: (snapshot as any)?.event?.type,
233
+ });
234
+ this.lastState = currentState;
235
+ this.state = snapshot;
236
+
237
+ this.updateUiControls(previousState !== currentState);
238
+ });
239
+
240
+ this.stateMachineService.start();
241
+ this.updateUiControls(true);
242
+ }
243
+
244
+ /**
245
+ * Send an event to the state machine
246
+ */
247
+ public sendStateMachineEvent(event: TaskEventPayload): void {
248
+ if (this.stateMachineService) {
249
+ LoggerProxy.log(`Sending state machine event: ${event?.type}`, {
250
+ module: CC_FILE,
251
+ method: 'sendStateMachineEvent',
252
+ interactionId: this.data?.interactionId,
253
+ });
254
+
255
+ this.stateMachineService.send(event);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get the current state machine state
261
+ */
262
+ protected getCurrentState(): TaskState | undefined {
263
+ return this.stateMachineService?.getSnapshot()?.value as TaskState;
264
+ }
265
+
266
+ /**
267
+ * Compute UI controls based on current state machine state.
268
+ *
269
+ * @returns UI control states for all task actions
270
+ */
271
+ protected computeUIControls(): TaskUIControls {
272
+ const snapshot = this.stateMachineService?.getSnapshot?.();
273
+
274
+ if (!snapshot) {
275
+ return getDefaultUIControls();
276
+ }
277
+
278
+ const currentState = snapshot.value as TaskState;
279
+ const context = snapshot.context as TaskContext;
280
+
281
+ return computeUIControls(currentState, context, this.data);
282
+ }
283
+
284
+ /**
285
+ * Stop the state machine service
286
+ */
287
+ protected stopStateMachine(): void {
288
+ if (this.stateMachineService) {
289
+ this.stateMachineService.stop();
290
+ this.stateMachineService = undefined;
291
+ }
292
+ }
293
+
294
+ private static extractTaskDataFromEvent(event?: TaskEventPayload): TaskData | undefined {
295
+ if (!event || typeof event !== 'object') {
296
+ return undefined;
297
+ }
298
+
299
+ if ('taskData' in event) {
300
+ return (event as {taskData?: TaskData}).taskData;
301
+ }
302
+
303
+ return undefined;
304
+ }
305
+
306
+ private async autoAnswerIfNeeded(): Promise<void> {
307
+ if (!this.data) {
308
+ return;
309
+ }
310
+
311
+ const autoAnswerSupported =
312
+ this.uiControlConfig.channelType === TASK_CHANNEL_TYPE.DIGITAL ||
313
+ this.uiControlConfig.voiceVariant === VOICE_VARIANT.WEBRTC;
314
+
315
+ if (!autoAnswerSupported) {
316
+ return;
317
+ }
318
+
319
+ const shouldAutoAnswer = this.data.isAutoAnswering === true;
320
+
321
+ if (!shouldAutoAnswer) {
322
+ return;
323
+ }
324
+
325
+ LoggerProxy.info(`Auto-answering task`, {
326
+ module: TASK_FILE,
327
+ method: 'autoAnswerIfNeeded',
328
+ interactionId: this.data.interactionId,
329
+ });
330
+
331
+ try {
332
+ await this.accept();
333
+ LoggerProxy.info(`Task auto-answered successfully`, {
334
+ module: TASK_FILE,
335
+ method: 'autoAnswerIfNeeded',
336
+ interactionId: this.data.interactionId,
337
+ });
338
+
339
+ this.metricsManager.trackEvent(
340
+ METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS,
341
+ {
342
+ taskId: this.data.interactionId,
343
+ mediaType: this.data.interaction.mediaType,
344
+ isAutoAnswered: true,
345
+ },
346
+ ['behavioral', 'operational']
347
+ );
348
+
349
+ this.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, this);
350
+ } catch (error) {
351
+ this.updateTaskData({...this.data, isAutoAnswering: false});
352
+ LoggerProxy.error(`Failed to auto-answer task`, {
353
+ module: TASK_FILE,
354
+ method: 'autoAnswerIfNeeded',
355
+ interactionId: this.data.interactionId,
356
+ error,
357
+ });
358
+
359
+ this.metricsManager.trackEvent(
360
+ METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED,
361
+ {
362
+ taskId: this.data.interactionId,
363
+ mediaType: this.data.interaction.mediaType,
364
+ error: error?.message || 'Unknown error',
365
+ isAutoAnswered: false,
366
+ },
367
+ ['behavioral', 'operational']
368
+ );
369
+ }
370
+ }
371
+
372
+ private updateTaskFromEvent(event?: TaskEventPayload): void {
373
+ const taskData = Task.extractTaskDataFromEvent(event);
374
+ if (taskData) {
375
+ this.updateTaskData(taskData);
376
+ }
377
+ }
378
+
379
+ protected getStateMachineActionOverrides(): Partial<TaskActionsMap> {
380
+ return {
381
+ ...this.getCommonActionOverrides(),
382
+ ...this.getChannelSpecificActionOverrides(),
383
+ };
384
+ }
385
+
386
+ protected getChannelSpecificActionOverrides(): Partial<TaskActionsMap> {
387
+ return {};
388
+ }
389
+
390
+ protected createEmitSelfAction(
391
+ taskEvent: TASK_EVENTS,
392
+ {updateTaskData = false}: {updateTaskData?: boolean} = {}
393
+ ) {
394
+ return ({event}: TaskActionArgs) => {
395
+ if (updateTaskData) {
396
+ this.updateTaskFromEvent(event);
397
+ }
398
+ LoggerProxy.info(`Emitting task event ${taskEvent}`, {
399
+ module: TASK_FILE,
400
+ method: 'emitTaskEvent',
401
+ interactionId: this.data?.interactionId,
402
+ });
403
+ this.emit(taskEvent, this);
404
+ };
405
+ }
406
+
407
+ private getCommonActionOverrides(): Partial<TaskActionsMap> {
408
+ return {
409
+ syncTaskDataFromEvent: ({event}: {event: TaskEventPayload}) => {
410
+ this.updateTaskFromEvent(event);
411
+ },
412
+ emitTaskIncoming: this.createEmitSelfAction(TASK_EVENTS.TASK_INCOMING, {
413
+ updateTaskData: true,
414
+ }),
415
+ emitTaskHydrate: this.createEmitSelfAction(TASK_EVENTS.TASK_HYDRATE, {
416
+ updateTaskData: true,
417
+ }),
418
+ emitTaskOfferContact: this.createEmitSelfAction(TASK_EVENTS.TASK_OFFER_CONTACT, {
419
+ updateTaskData: true,
420
+ }),
421
+ emitTaskAssigned: this.createEmitSelfAction(TASK_EVENTS.TASK_ASSIGNED, {
422
+ updateTaskData: true,
423
+ }),
424
+ emitTaskEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_END, {updateTaskData: true}),
425
+ emitTaskOfferConsult: this.createEmitSelfAction(TASK_EVENTS.TASK_OFFER_CONSULT, {
426
+ updateTaskData: true,
427
+ }),
428
+ emitTaskConsultCreated: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_CREATED, {
429
+ updateTaskData: true,
430
+ }),
431
+ emitTaskConsulting: ({event}: TaskActionArgs) => {
432
+ this.updateTaskFromEvent(event);
433
+ if (this.data.isConsulted) {
434
+ this.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this);
435
+ } else {
436
+ this.emit(TASK_EVENTS.TASK_CONSULTING, this);
437
+ }
438
+ },
439
+ emitTaskConsultAccepted: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_ACCEPTED),
440
+ emitTaskConsultEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_END, {
441
+ updateTaskData: true,
442
+ }),
443
+ emitTaskConsultQueueCancelled: this.createEmitSelfAction(
444
+ TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
445
+ {
446
+ updateTaskData: true,
447
+ }
448
+ ),
449
+ emitTaskConsultQueueFailed: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, {
450
+ updateTaskData: true,
451
+ }),
452
+ emitTaskReject: ({event}: TaskActionArgs) => {
453
+ this.updateTaskFromEvent(event);
454
+ const reason =
455
+ event && typeof event === 'object' && 'reason' in event
456
+ ? (event as {reason?: string}).reason
457
+ : undefined;
458
+ this.emit(TASK_EVENTS.TASK_REJECT, reason);
459
+ },
460
+ emitTaskWrapup: ({event}: {event?: TaskEventPayload}) => {
461
+ this.updateTaskFromEvent(event);
462
+
463
+ const shouldEmitWrapup = Boolean(this.data.wrapUpRequired);
464
+ if (!shouldEmitWrapup) {
465
+ LoggerProxy.info(`Skipping task:wrapup event - wrapUpRequired is false`, {
466
+ module: TASK_FILE,
467
+ method: 'emitTaskEvent',
468
+ interactionId: this.data?.interactionId,
469
+ });
470
+
471
+ return;
472
+ }
473
+ LoggerProxy.info(`Emitting task event ${TASK_EVENTS.TASK_WRAPUP}`, {
474
+ module: TASK_FILE,
475
+ method: 'emitTaskEvent',
476
+ interactionId: this.data?.interactionId,
477
+ });
478
+ this.emit(TASK_EVENTS.TASK_WRAPUP, this);
479
+ },
480
+ emitTaskWrappedup: this.createEmitSelfAction(TASK_EVENTS.TASK_WRAPPEDUP, {
481
+ updateTaskData: true,
482
+ }),
483
+ requestAutoAnswer: ({event}: TaskActionArgs) => {
484
+ if (event) {
485
+ // parameter intentionally unused
486
+ }
487
+ this.autoAnswerIfNeeded();
488
+ },
489
+ requestCleanup: () => {
490
+ this.emit(TASK_EVENTS.TASK_CLEANUP, this, {removeFromCollection: false});
491
+ },
492
+ cleanupResources: () => {
493
+ this.emit(TASK_EVENTS.TASK_CLEANUP, this, {removeFromCollection: true});
494
+ },
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Sets up the automatic wrap-up timer if wrap-up is required
500
+ */
501
+ protected setupAutoWrapupTimer(): void {
502
+ if (
503
+ this.data.wrapUpRequired &&
504
+ !this.autoWrapup &&
505
+ this.wrapupData &&
506
+ this.wrapupData.wrapUpProps
507
+ ) {
508
+ const wrapUpProps = this.wrapupData.wrapUpProps;
509
+ if (!wrapUpProps || wrapUpProps.autoWrapup === false) {
510
+ LoggerProxy.info(`Auto wrap-up is not required for this task`, {
511
+ module: TASK_FILE,
512
+ method: METHODS.SETUP_AUTO_WRAPUP_TIMER,
513
+ interactionId: this.data.interactionId,
514
+ });
515
+
516
+ return;
517
+ }
518
+ const defaultWrapupReason =
519
+ wrapUpProps.wrapUpReasonList?.find((r) => r.isDefault) ?? wrapUpProps.wrapUpReasonList?.[0];
520
+ if (!defaultWrapupReason) {
521
+ LoggerProxy.error('No wrap-up reason configured', {
522
+ module: TASK_FILE,
523
+ method: METHODS.SETUP_AUTO_WRAPUP_TIMER,
524
+ });
525
+
526
+ return;
527
+ }
528
+ const intervalMs = wrapUpProps.autoWrapupInterval;
529
+ if (!intervalMs || intervalMs <= 0) {
530
+ LoggerProxy.error(`Invalid auto wrap-up interval: ${intervalMs}`, {
531
+ module: TASK_FILE,
532
+ method: METHODS.SETUP_AUTO_WRAPUP_TIMER,
533
+ });
534
+
535
+ return;
536
+ }
537
+ this.autoWrapup = new AutoWrapup(intervalMs, wrapUpProps.allowCancelAutoWrapup);
538
+ this.autoWrapup.start(async () => {
539
+ LoggerProxy.info(`Auto wrap-up timer triggered`, {
540
+ module: TASK_FILE,
541
+ method: METHODS.SETUP_AUTO_WRAPUP_TIMER,
542
+ interactionId: this.data.interactionId,
543
+ });
544
+ await this.wrapup({
545
+ wrapUpReason: defaultWrapupReason.name,
546
+ auxCodeId: defaultWrapupReason.id,
547
+ });
548
+ });
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Cancels the automatic wrap-up timer if it's running
554
+ */
555
+ private reconcileData(oldData: TaskData, newData: TaskData): TaskData {
556
+ Object.keys(newData).forEach((key) => {
557
+ if (newData[key] && typeof newData[key] === 'object' && !Array.isArray(newData[key])) {
558
+ oldData[key] = this.reconcileData({...oldData[key]}, newData[key]);
559
+ } else {
560
+ oldData[key] = newData[key];
561
+ }
562
+ });
563
+
564
+ return oldData;
565
+ }
566
+
567
+ /**
568
+ *
569
+ * @param methodName - The name of the method that is unsupported
570
+ * @throws Error
571
+ */
572
+ protected unsupportedMethodError(methodName: string) {
573
+ LoggerProxy.error(`Unsupported operation`, {
574
+ module: 'TASK',
575
+ method: methodName,
576
+ interactionId: this.data?.interactionId,
577
+ });
578
+ throw new Error(`Unsupported operation: ${methodName}`);
579
+ }
580
+
581
+ /**
582
+ * This method is used to update the task data.
583
+ * @param updatedData - TaskData
584
+ * @param shouldOverwrite - boolean
585
+ * @example
586
+ * ```typescript
587
+ * task.updateTaskData(updatedData, true);
588
+ * ```
589
+ */
590
+ public updateTaskData(updatedData: TaskData, shouldOverwrite = false): ITask {
591
+ this.data = shouldOverwrite ? updatedData : this.reconcileData(this.data, updatedData);
592
+ this.updateUiControls();
593
+ this.setupAutoWrapupTimer();
594
+
595
+ return this;
596
+ }
597
+
598
+ /**
599
+ * This is used to blind transfer or vTeam transfer the task
600
+ * @param transferPayload
601
+ * @returns Promise<TaskResponse>
602
+ * @throws Error
603
+ * @example
604
+ * ```typescript
605
+ * const transferPayload = {
606
+ * to: 'myQueueId',
607
+ * destinationType: 'queue',
608
+ * }
609
+ * task.transfer(transferPayload).then(()=>{}).catch(()=>{});
610
+ * ```
611
+ */
612
+ public async transfer(transferPayload: TransferPayLoad): Promise<TaskResponse> {
613
+ LoggerProxy.log(`Starting task transfer for taskId:${this.data.interactionId}`, {
614
+ module: 'Task',
615
+ method: 'transfer',
616
+ });
617
+ try {
618
+ this.metricsManager.timeEvent([
619
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
620
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
621
+ ]);
622
+ let result: TaskResponse;
623
+ if (transferPayload.destinationType === DESTINATION_TYPE.QUEUE) {
624
+ result = await this.contact.vteamTransfer({
625
+ interactionId: this.data.interactionId,
626
+ data: transferPayload,
627
+ });
628
+ } else {
629
+ result = await this.contact.blindTransfer({
630
+ interactionId: this.data.interactionId,
631
+ data: transferPayload,
632
+ });
633
+ }
634
+
635
+ this.metricsManager.trackEvent(
636
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
637
+ {
638
+ taskId: this.data.interactionId,
639
+ destination: transferPayload.to,
640
+ destinationType: transferPayload.destinationType,
641
+ isConsultTransfer: false,
642
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
643
+ },
644
+ ['operational', 'behavioral', 'business']
645
+ );
646
+
647
+ return result;
648
+ } catch (error) {
649
+ const {error: detailedError} = getErrorDetails(error, 'transfer', CC_FILE);
650
+ this.metricsManager.trackEvent(
651
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
652
+ {
653
+ taskId: this.data.interactionId,
654
+ destination: transferPayload.to,
655
+ destinationType: transferPayload.destinationType,
656
+ isConsultTransfer: false,
657
+ error: error.toString(),
658
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
659
+ (error as any).details || {}
660
+ ),
661
+ },
662
+ ['operational', 'behavioral', 'business']
663
+ );
664
+ throw detailedError;
665
+ }
666
+ }
667
+
668
+ /**
669
+ * This is used to end the task.
670
+ * @returns Promise<TaskResponse>
671
+ * @throws Error
672
+ * @example
673
+ * ```typescript
674
+ * task.end().then(()=>{}).catch(()=>{})
675
+ * ```
676
+ */
677
+ public async end(): Promise<TaskResponse> {
678
+ LoggerProxy.log(`Ending task for taskId:${this.data.interactionId}`, {
679
+ module: 'Task',
680
+ method: 'end',
681
+ });
682
+ const requestInteractionId =
683
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
684
+
685
+ try {
686
+ this.metricsManager.timeEvent([
687
+ METRIC_EVENT_NAMES.TASK_END_SUCCESS,
688
+ METRIC_EVENT_NAMES.TASK_END_FAILED,
689
+ ]);
690
+ const response = await this.contact.end({interactionId: requestInteractionId});
691
+
692
+ this.metricsManager.trackEvent(
693
+ METRIC_EVENT_NAMES.TASK_END_SUCCESS,
694
+ {
695
+ taskId: this.data.interactionId,
696
+ requestInteractionId,
697
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
698
+ },
699
+ ['operational', 'behavioral', 'business']
700
+ );
701
+
702
+ return response;
703
+ } catch (error) {
704
+ const {error: detailedError} = getErrorDetails(error, 'end', CC_FILE);
705
+ this.metricsManager.trackEvent(
706
+ METRIC_EVENT_NAMES.TASK_END_FAILED,
707
+ {
708
+ taskId: this.data.interactionId,
709
+ requestInteractionId,
710
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
711
+ (error as any).details || {}
712
+ ),
713
+ },
714
+ ['operational', 'behavioral', 'business']
715
+ );
716
+ throw detailedError;
717
+ }
718
+ }
719
+
720
+ /**
721
+ * This is used to wrap up the task.
722
+ * @param wrapupPayload - WrapupPayLoad
723
+ * @returns Promise<TaskResponse>
724
+ * @throws Error
725
+ * @example
726
+ * ```typescript
727
+ * task.wrapup(wrapupPayload).then(()=>{}).catch(()=>{})
728
+ * ```
729
+ */
730
+ public async wrapup(wrapupPayload: WrapupPayLoad): Promise<TaskResponse> {
731
+ this.cancelAutoWrapupTimer();
732
+ LoggerProxy.log(`Starting task wrapup for taskId:${this.data.interactionId}`, {
733
+ module: 'Task',
734
+ method: 'wrapup',
735
+ });
736
+ try {
737
+ this.metricsManager.timeEvent([
738
+ METRIC_EVENT_NAMES.TASK_WRAPUP_SUCCESS,
739
+ METRIC_EVENT_NAMES.TASK_WRAPUP_FAILED,
740
+ ]);
741
+ if (!this.data) {
742
+ throw new Error('No task data available');
743
+ }
744
+ if (!wrapupPayload.auxCodeId || wrapupPayload.auxCodeId.length === 0) {
745
+ throw new Error('AuxCodeId is required');
746
+ }
747
+ if (!wrapupPayload.wrapUpReason || wrapupPayload.wrapUpReason.length === 0) {
748
+ throw new Error('WrapUpReason is required');
749
+ }
750
+
751
+ const response = await this.contact.wrapup({
752
+ interactionId: this.data.interactionId,
753
+ data: wrapupPayload,
754
+ });
755
+
756
+ this.metricsManager.trackEvent(
757
+ METRIC_EVENT_NAMES.TASK_WRAPUP_SUCCESS,
758
+ {
759
+ taskId: this.data.interactionId,
760
+ wrapUpCode: wrapupPayload.auxCodeId,
761
+ wrapUpReason: wrapupPayload.wrapUpReason,
762
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
763
+ },
764
+ ['operational', 'behavioral', 'business']
765
+ );
766
+
767
+ return response;
768
+ } catch (error) {
769
+ const {error: detailedError} = getErrorDetails(error, 'wrapup', CC_FILE);
770
+ this.metricsManager.trackEvent(
771
+ METRIC_EVENT_NAMES.TASK_WRAPUP_FAILED,
772
+ {
773
+ taskId: this.data.interactionId,
774
+ wrapUpCode: wrapupPayload.auxCodeId,
775
+ wrapUpReason: wrapupPayload.wrapUpReason,
776
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
777
+ (error as any).details || {}
778
+ ),
779
+ },
780
+ ['operational', 'behavioral', 'business']
781
+ );
782
+ throw detailedError;
783
+ }
784
+ }
785
+ }