@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11

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 (198) hide show
  1. package/.sdd/manifest.json +7 -5
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +8 -3
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +544 -12
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/MetricsManager.js +16 -0
  15. package/dist/metrics/MetricsManager.js.map +1 -1
  16. package/dist/metrics/behavioral-events.js +104 -0
  17. package/dist/metrics/behavioral-events.js.map +1 -1
  18. package/dist/metrics/constants.js +18 -0
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/services/AddressBook.js +18 -15
  21. package/dist/services/AddressBook.js.map +1 -1
  22. package/dist/services/AnswerCallOnWebexService.js +202 -0
  23. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  24. package/dist/services/EntryPoint.js +46 -68
  25. package/dist/services/EntryPoint.js.map +1 -1
  26. package/dist/services/Queue.js +27 -22
  27. package/dist/services/Queue.js.map +1 -1
  28. package/dist/services/WebexCrossClientService.js +222 -0
  29. package/dist/services/WebexCrossClientService.js.map +1 -0
  30. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  31. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  32. package/dist/services/config/Util.js +3 -0
  33. package/dist/services/config/Util.js.map +1 -1
  34. package/dist/services/config/constants.js +10 -6
  35. package/dist/services/config/constants.js.map +1 -1
  36. package/dist/services/config/types.js +4 -0
  37. package/dist/services/config/types.js.map +1 -1
  38. package/dist/services/core/Err.js.map +1 -1
  39. package/dist/services/core/WebexRequest.js +6 -2
  40. package/dist/services/core/WebexRequest.js.map +1 -1
  41. package/dist/services/core/aqm-reqs.js +28 -14
  42. package/dist/services/core/aqm-reqs.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/task/Task.js +73 -7
  45. package/dist/services/task/Task.js.map +1 -1
  46. package/dist/services/task/TaskFactory.js +8 -4
  47. package/dist/services/task/TaskFactory.js.map +1 -1
  48. package/dist/services/task/TaskManager.js +490 -21
  49. package/dist/services/task/TaskManager.js.map +1 -1
  50. package/dist/services/task/WebexCallingUtils.js +70 -0
  51. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  52. package/dist/services/task/constants.js +4 -1
  53. package/dist/services/task/constants.js.map +1 -1
  54. package/dist/services/task/contact.js +29 -0
  55. package/dist/services/task/contact.js.map +1 -1
  56. package/dist/services/task/digital/Digital.js +3 -2
  57. package/dist/services/task/digital/Digital.js.map +1 -1
  58. package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
  59. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  60. package/dist/services/task/state-machine/actions.js +6 -1
  61. package/dist/services/task/state-machine/actions.js.map +1 -1
  62. package/dist/services/task/state-machine/guards.js +7 -10
  63. package/dist/services/task/state-machine/guards.js.map +1 -1
  64. package/dist/services/task/state-machine/types.js.map +1 -1
  65. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  66. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  67. package/dist/services/task/types.js +16 -1
  68. package/dist/services/task/types.js.map +1 -1
  69. package/dist/services/task/voice/Voice.js +456 -15
  70. package/dist/services/task/voice/Voice.js.map +1 -1
  71. package/dist/services/task/voice/WebRTC.js +4 -1
  72. package/dist/services/task/voice/WebRTC.js.map +1 -1
  73. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  74. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  75. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  76. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  77. package/dist/services/wxAppTelephonyUtils.js +19 -0
  78. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  79. package/dist/types/cc.d.ts +78 -4
  80. package/dist/types/config.d.ts +7 -0
  81. package/dist/types/constants.d.ts +8 -0
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  84. package/dist/types/metrics/constants.d.ts +17 -0
  85. package/dist/types/services/AddressBook.d.ts +2 -1
  86. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  87. package/dist/types/services/EntryPoint.d.ts +7 -5
  88. package/dist/types/services/Queue.d.ts +5 -3
  89. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  90. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  91. package/dist/types/services/config/constants.d.ts +8 -5
  92. package/dist/types/services/config/types.d.ts +17 -4
  93. package/dist/types/services/core/Err.d.ts +2 -0
  94. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  95. package/dist/types/services/core/types.d.ts +2 -0
  96. package/dist/types/services/task/Task.d.ts +20 -2
  97. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  98. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  99. package/dist/types/services/task/constants.d.ts +3 -0
  100. package/dist/types/services/task/contact.d.ts +4 -0
  101. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  102. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
  103. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  104. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  105. package/dist/types/services/task/types.d.ts +81 -4
  106. package/dist/types/services/task/voice/Voice.d.ts +52 -9
  107. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  108. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  109. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  110. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  111. package/dist/types/types.d.ts +14 -8
  112. package/dist/types/utils/PageCache.d.ts +20 -3
  113. package/dist/types.js +7 -2
  114. package/dist/types.js.map +1 -1
  115. package/dist/utils/PageCache.js +19 -5
  116. package/dist/utils/PageCache.js.map +1 -1
  117. package/dist/webex.js +1 -1
  118. package/package.json +8 -8
  119. package/src/cc.ts +657 -12
  120. package/src/config.ts +7 -0
  121. package/src/constants.ts +8 -0
  122. package/src/index.ts +2 -0
  123. package/src/metrics/MetricsManager.ts +16 -0
  124. package/src/metrics/ai-docs/metrics-spec.md +78 -10
  125. package/src/metrics/behavioral-events.ts +106 -0
  126. package/src/metrics/constants.ts +20 -0
  127. package/src/services/AddressBook.ts +17 -6
  128. package/src/services/AnswerCallOnWebexService.ts +239 -0
  129. package/src/services/EntryPoint.ts +59 -60
  130. package/src/services/Queue.ts +29 -12
  131. package/src/services/WebexCrossClientService.ts +293 -0
  132. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  133. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  134. package/src/services/ai-docs/AGENTS.md +10 -10
  135. package/src/services/ai-docs/services-spec.md +6 -1
  136. package/src/services/config/Util.ts +3 -0
  137. package/src/services/config/ai-docs/AGENTS.md +1 -1
  138. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  139. package/src/services/config/ai-docs/config-spec.md +6 -0
  140. package/src/services/config/constants.ts +10 -6
  141. package/src/services/config/types.ts +16 -4
  142. package/src/services/core/Err.ts +1 -0
  143. package/src/services/core/WebexRequest.ts +3 -1
  144. package/src/services/core/ai-docs/core-spec.md +5 -1
  145. package/src/services/core/aqm-reqs.ts +30 -15
  146. package/src/services/core/types.ts +2 -0
  147. package/src/services/task/Task.ts +79 -8
  148. package/src/services/task/TaskFactory.ts +8 -3
  149. package/src/services/task/TaskManager.ts +716 -17
  150. package/src/services/task/WebexCallingUtils.ts +136 -0
  151. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  152. package/src/services/task/ai-docs/task-spec.md +156 -3
  153. package/src/services/task/constants.ts +3 -0
  154. package/src/services/task/contact.ts +30 -0
  155. package/src/services/task/digital/Digital.ts +4 -2
  156. package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
  157. package/src/services/task/state-machine/actions.ts +5 -2
  158. package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
  159. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
  160. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
  161. package/src/services/task/state-machine/guards.ts +7 -10
  162. package/src/services/task/state-machine/types.ts +16 -1
  163. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  164. package/src/services/task/types.ts +114 -4
  165. package/src/services/task/voice/Voice.ts +595 -14
  166. package/src/services/task/voice/WebRTC.ts +5 -1
  167. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  168. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  169. package/src/services/wxAppTelephonyUtils.ts +14 -0
  170. package/src/types.ts +33 -9
  171. package/src/utils/AGENTS.md +21 -10
  172. package/src/utils/PageCache.ts +38 -5
  173. package/src/utils/ai-docs/utils-spec.md +21 -11
  174. package/test/unit/spec/cc.ts +1522 -19
  175. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  176. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  177. package/test/unit/spec/services/AddressBook.ts +37 -6
  178. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  179. package/test/unit/spec/services/EntryPoint.ts +87 -40
  180. package/test/unit/spec/services/Queue.ts +123 -12
  181. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  182. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  183. package/test/unit/spec/services/config/Util.ts +85 -0
  184. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  185. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  186. package/test/unit/spec/services/task/Task.ts +274 -6
  187. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  188. package/test/unit/spec/services/task/TaskManager.ts +1717 -9
  189. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  190. package/test/unit/spec/services/task/contact.ts +33 -0
  191. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
  192. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  193. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  194. package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
  195. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  196. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  197. package/umd/contact-center.min.js +2 -2
  198. package/umd/contact-center.min.js.map +1 -1
@@ -0,0 +1,516 @@
1
+ import {EventPayload} from '@webex/internal-plugin-metrics/src/metrics.types';
2
+
3
+ import AnswerCallOnWebexService, {WxAppTelephonyError} from '../../AnswerCallOnWebexService';
4
+ import LoggerProxy from '../../../logger-proxy';
5
+ import {METHODS} from '../../../constants';
6
+ import {getErrorDetails} from '../../core/Utils';
7
+ import {Failure} from '../../core/GlobalTypes';
8
+ import {TaskData, TaskToggleMuteOptions, TaskTransmitDtmfOptions} from '../types';
9
+ import {TaskState} from '../state-machine';
10
+ import MetricsManager from '../../../metrics/MetricsManager';
11
+ import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
12
+ import {
13
+ decodedLineOwnerId,
14
+ getWebexCallingDeviceDetailsForAgent,
15
+ WebexCallingDeviceDetails,
16
+ } from '../WebexCallingUtils';
17
+ import {
18
+ logWxAppTelephonyAction,
19
+ logWxAppValidationFailure,
20
+ type WxAppTelephonyAction,
21
+ } from '../../wxAppDiagnosticLogging';
22
+
23
+ type WxAppParticipant = {
24
+ deviceType?: string;
25
+ deviceCallId?: string;
26
+ deviceId?: string;
27
+ };
28
+
29
+ export type WxAppVoiceDependencies = {
30
+ enableWxBetterTogether: boolean;
31
+ answerCallOnWebexService?: AnswerCallOnWebexService;
32
+ agentId?: string;
33
+ metricsManager: MetricsManager;
34
+ getTaskData: () => TaskData;
35
+ getTaskState: () => TaskState | undefined;
36
+ getWxAppMuted: () => boolean;
37
+ setWxAppMuted: (muted: boolean) => void;
38
+ };
39
+
40
+ /** @deprecated Use {@link WxAppVoiceDependencies} */
41
+ export type WxAppVoiceDeps = WxAppVoiceDependencies;
42
+
43
+ export type WxAppVoiceLifecycle = {
44
+ setWxAppAcceptInFlight: (inFlight: boolean) => void;
45
+ setWxAppAnswerPending: (pending: boolean) => void;
46
+ resetWxAppMuted: () => void;
47
+ syncWxAppMuteFromCallDetails: () => Promise<boolean | undefined>;
48
+ mapWxAppVoiceError: (error: unknown, method: string) => never;
49
+ };
50
+
51
+ const isWxAppParticipant = (participant: unknown): participant is WxAppParticipant =>
52
+ (participant as WxAppParticipant)?.deviceType === 'wxApp' &&
53
+ typeof (participant as WxAppParticipant)?.deviceCallId === 'string' &&
54
+ (participant as WxAppParticipant).deviceCallId.trim() !== '' &&
55
+ typeof (participant as WxAppParticipant)?.deviceId === 'string' &&
56
+ (participant as WxAppParticipant).deviceId.trim() !== '';
57
+
58
+ const isOutdialTask = (deps: WxAppVoiceDependencies): boolean =>
59
+ deps.getTaskData()?.interaction?.outboundType === 'OUTDIAL';
60
+
61
+ function getWxAppAgentParticipant(deps: WxAppVoiceDependencies): unknown {
62
+ const taskData = deps.getTaskData();
63
+ const participants = taskData.interaction?.participants;
64
+ const agentId = deps.agentId ?? taskData.agentId;
65
+
66
+ if (!participants || !agentId) {
67
+ return undefined;
68
+ }
69
+
70
+ if (participants[agentId]) {
71
+ return participants[agentId];
72
+ }
73
+
74
+ return Object.values(participants).find(
75
+ (participant) => (participant as {id?: string})?.id === agentId
76
+ );
77
+ }
78
+
79
+ export function getCallingDeviceDetails(
80
+ deps: WxAppVoiceDependencies
81
+ ): WebexCallingDeviceDetails | undefined {
82
+ const taskData = deps.getTaskData();
83
+ const participant = getWxAppAgentParticipant(deps);
84
+
85
+ if (!isWxAppParticipant(participant)) {
86
+ return undefined;
87
+ }
88
+
89
+ return getWebexCallingDeviceDetailsForAgent(
90
+ deps.agentId ?? taskData.agentId,
91
+ taskData.interaction?.participants
92
+ );
93
+ }
94
+
95
+ export function isWebexAppCallingOffer(deps: WxAppVoiceDependencies): boolean {
96
+ if (!deps.enableWxBetterTogether) {
97
+ return false;
98
+ }
99
+
100
+ const state = deps.getTaskState();
101
+ if (state !== TaskState.OFFERED) {
102
+ return false;
103
+ }
104
+
105
+ const details = getCallingDeviceDetails(deps);
106
+
107
+ return Boolean(details);
108
+ }
109
+
110
+ /** Inbound wxApp offer only — outdial decline uses CC routing, not telephony reject. */
111
+ export function isWebexAppInboundCallingOffer(deps: WxAppVoiceDependencies): boolean {
112
+ return isWebexAppCallingOffer(deps) && !isOutdialTask(deps);
113
+ }
114
+
115
+ export function getWebexCallingCallId(deps: WxAppVoiceDependencies): string | null {
116
+ const state = deps.getTaskState();
117
+ if (
118
+ !state ||
119
+ state === TaskState.OFFERED ||
120
+ state === TaskState.IDLE ||
121
+ state === TaskState.WRAPPING_UP ||
122
+ state === TaskState.TERMINATED ||
123
+ state === TaskState.COMPLETED
124
+ ) {
125
+ return null;
126
+ }
127
+
128
+ return getCallingDeviceDetails(deps)?.deviceCallId ?? null;
129
+ }
130
+
131
+ export function getWxAppLineOwnerId(deps: WxAppVoiceDependencies): string | undefined {
132
+ const participant = getWxAppAgentParticipant(deps) as {lineOwnerId?: string} | undefined;
133
+ const lineOwnerId = participant?.lineOwnerId;
134
+
135
+ return typeof lineOwnerId === 'string' && lineOwnerId.trim() !== '' ? lineOwnerId : undefined;
136
+ }
137
+
138
+ export function resolveWxAppLineOwnerId(
139
+ deps: WxAppVoiceDependencies,
140
+ override?: string
141
+ ): string | undefined {
142
+ if (typeof override === 'string' && override.trim() !== '') {
143
+ return override;
144
+ }
145
+
146
+ return decodedLineOwnerId(getWxAppLineOwnerId(deps));
147
+ }
148
+
149
+ function getInteractionId(deps: WxAppVoiceDependencies): string | undefined {
150
+ return deps.getTaskData()?.interactionId;
151
+ }
152
+
153
+ function logTelephonyFailure(
154
+ action: WxAppTelephonyAction,
155
+ deps: WxAppVoiceDependencies,
156
+ error: unknown
157
+ ): void {
158
+ const wxError = error as WxAppTelephonyError;
159
+ logWxAppTelephonyAction({
160
+ action,
161
+ phase: 'failed',
162
+ interactionId: getInteractionId(deps),
163
+ trackingId: wxError.trackingId,
164
+ httpStatus: wxError.status,
165
+ failureReason: wxError.message,
166
+ });
167
+ }
168
+
169
+ function getWxAppTelephonyMetricFailurePayload(
170
+ deps: WxAppVoiceDependencies,
171
+ error: unknown
172
+ ): Record<string, string> {
173
+ const wxError = error as WxAppTelephonyError;
174
+ const payload: Record<string, string> = {
175
+ taskId: getInteractionId(deps) ?? '',
176
+ error: error instanceof Error ? error.toString() : String(error),
177
+ };
178
+
179
+ if (wxError.trackingId) {
180
+ payload.trackingId = wxError.trackingId;
181
+ }
182
+
183
+ return payload;
184
+ }
185
+
186
+ type AqmWrappedError = {
187
+ details?: Failure;
188
+ };
189
+
190
+ function getWxAppOutdialDeclineFailurePayload(
191
+ deps: WxAppVoiceDependencies,
192
+ error: unknown
193
+ ): EventPayload {
194
+ const base: EventPayload = {
195
+ taskId: getInteractionId(deps) ?? '',
196
+ error: error instanceof Error ? error.toString() : String(error),
197
+ };
198
+ const aqmDetails = (error as AqmWrappedError).details;
199
+
200
+ if (aqmDetails) {
201
+ return {...base, ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(aqmDetails)};
202
+ }
203
+
204
+ return getWxAppTelephonyMetricFailurePayload(deps, error);
205
+ }
206
+
207
+ function logWxAppOutdialDeclineFailure(deps: WxAppVoiceDependencies, error: unknown): void {
208
+ const aqmDetails = (error as AqmWrappedError).details;
209
+ const wxError = error as WxAppTelephonyError;
210
+
211
+ logWxAppTelephonyAction({
212
+ action: 'decline',
213
+ phase: 'failed',
214
+ interactionId: getInteractionId(deps),
215
+ trackingId: aqmDetails?.trackingId ?? wxError.trackingId,
216
+ httpStatus: wxError.status,
217
+ failureReason: aqmDetails?.data?.reason ?? wxError.message,
218
+ });
219
+ }
220
+
221
+ function assertWxAppEnabled(deps: WxAppVoiceDependencies): void {
222
+ if (!deps.enableWxBetterTogether) {
223
+ logWxAppValidationFailure('enableWxBetterTogether_disabled', getInteractionId(deps));
224
+ throw new Error('enableWxBetterTogether is disabled');
225
+ }
226
+
227
+ if (!deps.answerCallOnWebexService) {
228
+ logWxAppValidationFailure('answer_call_on_webex_service_unavailable', getInteractionId(deps));
229
+ throw new Error('AnswerCallOnWebexService is unavailable');
230
+ }
231
+ }
232
+
233
+ export async function acceptOnWebex(
234
+ deps: WxAppVoiceDependencies,
235
+ options?: {lineOwnerId?: string}
236
+ ): Promise<void> {
237
+ const interactionId = getInteractionId(deps);
238
+ logWxAppTelephonyAction({action: 'accept', phase: 'started', interactionId});
239
+ assertWxAppEnabled(deps);
240
+
241
+ if (!isWebexAppCallingOffer(deps)) {
242
+ logWxAppValidationFailure('not_wxApp_calling_offer', interactionId);
243
+ throw new Error('Task is not a wxApp calling offer');
244
+ }
245
+
246
+ const details = getCallingDeviceDetails(deps);
247
+ if (!details) {
248
+ logWxAppValidationFailure('wxApp_device_details_unavailable', interactionId);
249
+ throw new Error('WxApp calling device details are unavailable');
250
+ }
251
+
252
+ await deps.answerCallOnWebexService.answerCall({
253
+ callId: details.deviceCallId,
254
+ endpointId: details.deviceId,
255
+ lineOwnerId: resolveWxAppLineOwnerId(deps, options?.lineOwnerId),
256
+ });
257
+ logWxAppTelephonyAction({action: 'accept', phase: 'success', interactionId});
258
+ }
259
+
260
+ export async function rejectOnWebex(
261
+ deps: WxAppVoiceDependencies,
262
+ options?: {lineOwnerId?: string}
263
+ ): Promise<void> {
264
+ const interactionId = getInteractionId(deps);
265
+ logWxAppTelephonyAction({action: 'decline', phase: 'started', interactionId});
266
+ assertWxAppEnabled(deps);
267
+
268
+ if (!isWebexAppInboundCallingOffer(deps)) {
269
+ logWxAppValidationFailure('not_wxApp_inbound_calling_offer', interactionId);
270
+ throw new Error('Task is not a wxApp inbound calling offer');
271
+ }
272
+
273
+ const details = getCallingDeviceDetails(deps);
274
+ if (!details) {
275
+ logWxAppValidationFailure('wxApp_device_details_unavailable', interactionId);
276
+ throw new Error('WxApp calling device details are unavailable');
277
+ }
278
+
279
+ await deps.answerCallOnWebexService.rejectCall({
280
+ callId: details.deviceCallId,
281
+ lineOwnerId: resolveWxAppLineOwnerId(deps, options?.lineOwnerId),
282
+ });
283
+ logWxAppTelephonyAction({action: 'decline', phase: 'success', interactionId});
284
+ }
285
+
286
+ export async function toggleMuteOnWebex(
287
+ deps: WxAppVoiceDependencies,
288
+ options?: {lineOwnerId?: string; muted?: boolean}
289
+ ): Promise<void> {
290
+ const interactionId = getInteractionId(deps);
291
+ logWxAppTelephonyAction({action: 'mute', phase: 'started', interactionId});
292
+ assertWxAppEnabled(deps);
293
+
294
+ const callId = getWebexCallingCallId(deps);
295
+ if (!callId) {
296
+ logWxAppValidationFailure('wxApp_call_id_unavailable', interactionId);
297
+ throw new Error('WxApp call ID is unavailable');
298
+ }
299
+
300
+ const targetMuted = options?.muted ?? !deps.getWxAppMuted();
301
+ const request = targetMuted
302
+ ? deps.answerCallOnWebexService.muteCall.bind(deps.answerCallOnWebexService)
303
+ : deps.answerCallOnWebexService.unmuteCall.bind(deps.answerCallOnWebexService);
304
+
305
+ await request({
306
+ callId,
307
+ lineOwnerId: resolveWxAppLineOwnerId(deps, options?.lineOwnerId),
308
+ });
309
+ deps.setWxAppMuted(targetMuted);
310
+ logWxAppTelephonyAction({action: 'mute', phase: 'success', interactionId});
311
+ }
312
+
313
+ export async function transmitDtmfOnWebex(
314
+ deps: WxAppVoiceDependencies,
315
+ options: {dtmf: string; lineOwnerId?: string}
316
+ ): Promise<void> {
317
+ const interactionId = getInteractionId(deps);
318
+ logWxAppTelephonyAction({action: 'dtmf', phase: 'started', interactionId});
319
+ assertWxAppEnabled(deps);
320
+
321
+ const callId = getWebexCallingCallId(deps);
322
+ if (!callId) {
323
+ logWxAppValidationFailure('wxApp_call_id_unavailable', interactionId);
324
+ throw new Error('WxApp call ID is unavailable');
325
+ }
326
+
327
+ LoggerProxy.info('transmitDtmf', {
328
+ module: 'wxAppVoiceMethods',
329
+ method: METHODS.TRANSMIT_DTMF,
330
+ data: {
331
+ interactionId,
332
+ callIdSuffix: callId.length <= 8 ? callId : callId.slice(-8),
333
+ dtmfLength: options.dtmf.length,
334
+ hasLineOwnerId: Boolean(options.lineOwnerId),
335
+ },
336
+ });
337
+
338
+ await deps.answerCallOnWebexService.transmitDtmf({
339
+ callId,
340
+ dtmf: options.dtmf,
341
+ lineOwnerId: resolveWxAppLineOwnerId(deps, options.lineOwnerId),
342
+ });
343
+ logWxAppTelephonyAction({action: 'dtmf', phase: 'success', interactionId});
344
+ }
345
+
346
+ export async function runWxAppAccept(
347
+ deps: WxAppVoiceDependencies,
348
+ lifecycle: WxAppVoiceLifecycle,
349
+ options?: {lineOwnerId?: string}
350
+ ): Promise<void> {
351
+ const taskId = getInteractionId(deps);
352
+
353
+ deps.metricsManager.timeEvent([
354
+ METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
355
+ METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED,
356
+ ]);
357
+
358
+ try {
359
+ lifecycle.setWxAppAcceptInFlight(true);
360
+ lifecycle.setWxAppAnswerPending(true);
361
+ await acceptOnWebex(deps, options);
362
+ lifecycle.resetWxAppMuted();
363
+ await lifecycle.syncWxAppMuteFromCallDetails();
364
+
365
+ deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS, {taskId}, [
366
+ 'operational',
367
+ 'behavioral',
368
+ ]);
369
+ } catch (error) {
370
+ lifecycle.setWxAppAnswerPending(false);
371
+ logTelephonyFailure('accept', deps, error);
372
+ deps.metricsManager.trackEvent(
373
+ METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED,
374
+ getWxAppTelephonyMetricFailurePayload(deps, error),
375
+ ['operational', 'behavioral']
376
+ );
377
+ lifecycle.mapWxAppVoiceError(error, METHODS.ACCEPT);
378
+ } finally {
379
+ lifecycle.setWxAppAcceptInFlight(false);
380
+ }
381
+ }
382
+
383
+ export async function runWxAppReject(
384
+ deps: WxAppVoiceDependencies,
385
+ lifecycle: WxAppVoiceLifecycle,
386
+ options?: {lineOwnerId?: string}
387
+ ): Promise<void> {
388
+ const taskId = getInteractionId(deps);
389
+
390
+ deps.metricsManager.timeEvent([
391
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
392
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
393
+ ]);
394
+
395
+ try {
396
+ await rejectOnWebex(deps, options);
397
+
398
+ deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS, {taskId}, [
399
+ 'operational',
400
+ 'behavioral',
401
+ ]);
402
+ } catch (error) {
403
+ logTelephonyFailure('decline', deps, error);
404
+ deps.metricsManager.trackEvent(
405
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
406
+ getWxAppTelephonyMetricFailurePayload(deps, error),
407
+ ['operational', 'behavioral']
408
+ );
409
+ lifecycle.mapWxAppVoiceError(error, METHODS.REJECT);
410
+ }
411
+ }
412
+
413
+ export async function runWxAppOutdialDecline<T>(
414
+ deps: WxAppVoiceDependencies,
415
+ executeCancel: () => Promise<T>
416
+ ): Promise<T> {
417
+ const taskId = getInteractionId(deps);
418
+
419
+ deps.metricsManager.timeEvent([
420
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
421
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
422
+ ]);
423
+
424
+ try {
425
+ const result = await executeCancel();
426
+
427
+ deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS, {taskId}, [
428
+ 'operational',
429
+ 'behavioral',
430
+ ]);
431
+
432
+ return result;
433
+ } catch (error) {
434
+ logWxAppOutdialDeclineFailure(deps, error);
435
+ deps.metricsManager.trackEvent(
436
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
437
+ getWxAppOutdialDeclineFailurePayload(deps, error),
438
+ ['operational', 'behavioral']
439
+ );
440
+ throw error;
441
+ }
442
+ }
443
+
444
+ export async function runWxAppToggleMute(
445
+ deps: WxAppVoiceDependencies,
446
+ lifecycle: WxAppVoiceLifecycle,
447
+ options?: TaskToggleMuteOptions
448
+ ): Promise<void> {
449
+ const taskId = getInteractionId(deps);
450
+
451
+ deps.metricsManager.timeEvent([
452
+ METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS,
453
+ METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_FAILED,
454
+ ]);
455
+
456
+ try {
457
+ await toggleMuteOnWebex(deps, options);
458
+
459
+ deps.metricsManager.trackEvent(
460
+ METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS,
461
+ {taskId, targetMuted: deps.getWxAppMuted()},
462
+ ['operational', 'behavioral']
463
+ );
464
+ } catch (error) {
465
+ logTelephonyFailure('mute', deps, error);
466
+ deps.metricsManager.trackEvent(
467
+ METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_FAILED,
468
+ getWxAppTelephonyMetricFailurePayload(deps, error),
469
+ ['operational', 'behavioral']
470
+ );
471
+ lifecycle.mapWxAppVoiceError(error, METHODS.TOGGLE_MUTE);
472
+ }
473
+ }
474
+
475
+ export async function runWxAppTransmitDtmf(
476
+ deps: WxAppVoiceDependencies,
477
+ lifecycle: WxAppVoiceLifecycle,
478
+ options: TaskTransmitDtmfOptions
479
+ ): Promise<void> {
480
+ const taskId = getInteractionId(deps);
481
+
482
+ deps.metricsManager.timeEvent([
483
+ METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS,
484
+ METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_FAILED,
485
+ ]);
486
+
487
+ try {
488
+ await transmitDtmfOnWebex(deps, options);
489
+
490
+ deps.metricsManager.trackEvent(
491
+ METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS,
492
+ {taskId, dtmfLength: options.dtmf.length},
493
+ ['operational', 'behavioral']
494
+ );
495
+ } catch (error) {
496
+ logTelephonyFailure('dtmf', deps, error);
497
+ deps.metricsManager.trackEvent(
498
+ METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_FAILED,
499
+ getWxAppTelephonyMetricFailurePayload(deps, error),
500
+ ['operational', 'behavioral']
501
+ );
502
+ lifecycle.mapWxAppVoiceError(error, METHODS.TRANSMIT_DTMF);
503
+ }
504
+ }
505
+
506
+ export function mapWxAppVoiceError(error: unknown, method: string, module: string): never {
507
+ if (
508
+ error instanceof Error &&
509
+ (error as Error & {isWxAppTelephonyError?: boolean}).isWxAppTelephonyError
510
+ ) {
511
+ throw error;
512
+ }
513
+
514
+ const {error: detailedError} = getErrorDetails(error, method, module);
515
+ throw detailedError;
516
+ }
@@ -0,0 +1,174 @@
1
+ import LoggerProxy from '../logger-proxy';
2
+
3
+ export const WXAPP_LOG_FEATURE = 'wxApp';
4
+ export const WXAPP_LOG_PREFIX = '[CC wxApp]';
5
+
6
+ export type WxAppAcceptReason =
7
+ | 'wxApp_offer_ready'
8
+ | 'wxApp_answer_pending'
9
+ | 'wxApp_accept_in_flight'
10
+ | 'extension_non_wxApp_offer'
11
+ | 'browser_webrtc_offer'
12
+ | 'outdial_auto_answer';
13
+
14
+ export type WxAppSessionSkipReason =
15
+ | 'unsupported_browser_login'
16
+ | 'flag_disabled'
17
+ | 'mercury_subscribe_failed'
18
+ | 'publish_failed';
19
+
20
+ export type WxAppTelephonyAction = 'accept' | 'decline' | 'mute' | 'dtmf';
21
+ export type WxAppTelephonyPhase = 'started' | 'success' | 'failed';
22
+
23
+ type WxAppLogData = Record<string, string | boolean | number | undefined>;
24
+
25
+ /** Last 8 chars of callId for correlation without logging full id. */
26
+ export function callIdSuffix(callId: string | null | undefined): string | undefined {
27
+ if (!callId) {
28
+ return undefined;
29
+ }
30
+
31
+ return callId.length <= 8 ? callId : callId.slice(-8);
32
+ }
33
+
34
+ function logWxApp(message: string, method: string, data: WxAppLogData): void {
35
+ LoggerProxy.info(`${WXAPP_LOG_PREFIX} ${message}`, {
36
+ module: 'wxAppDiagnosticLogging',
37
+ method,
38
+ data: {feature: WXAPP_LOG_FEATURE, ...data},
39
+ });
40
+ }
41
+
42
+ function logWxAppError(message: string, method: string, data: WxAppLogData): void {
43
+ LoggerProxy.error(`${WXAPP_LOG_PREFIX} ${message}`, {
44
+ module: 'wxAppDiagnosticLogging',
45
+ method,
46
+ data: {feature: WXAPP_LOG_FEATURE, ...data},
47
+ });
48
+ }
49
+
50
+ export function logWxAppSessionReadiness(params: {
51
+ enableWxBetterTogether: boolean;
52
+ loginOption: string | undefined;
53
+ wxAppHooksApplied: boolean;
54
+ usersubPublished: boolean;
55
+ mercurySubscribed: boolean;
56
+ telephonyTaskType: 'Voice' | 'WebRTC' | 'unknown';
57
+ skipReason?: WxAppSessionSkipReason;
58
+ }): void {
59
+ logWxApp('session readiness', 'logWxAppSessionReadiness', {
60
+ event: 'session_readiness',
61
+ enableWxBetterTogether: params.enableWxBetterTogether,
62
+ loginOption: params.loginOption,
63
+ wxAppHooksApplied: params.wxAppHooksApplied,
64
+ usersubPublished: params.usersubPublished,
65
+ mercurySubscribed: params.mercurySubscribed,
66
+ telephonyTaskType: params.telephonyTaskType,
67
+ skipReason: params.skipReason,
68
+ });
69
+ }
70
+
71
+ export function deriveWxAppAcceptReason(params: {
72
+ isWxAppInboundOffer: boolean;
73
+ isWxAppOutdialOffer: boolean;
74
+ isWebrtc: boolean;
75
+ isOutdial: boolean;
76
+ wxAppAcceptInFlight: boolean;
77
+ wxAppAnswerPending: boolean;
78
+ enableWxBetterTogether: boolean;
79
+ hasDeviceCallId: boolean;
80
+ }): WxAppAcceptReason {
81
+ if (params.isWxAppInboundOffer || params.isWxAppOutdialOffer) {
82
+ if (params.wxAppAcceptInFlight) {
83
+ return 'wxApp_accept_in_flight';
84
+ }
85
+ if (params.wxAppAnswerPending) {
86
+ return 'wxApp_answer_pending';
87
+ }
88
+
89
+ return 'wxApp_offer_ready';
90
+ }
91
+
92
+ if (params.isWebrtc && !params.isOutdial) {
93
+ return 'browser_webrtc_offer';
94
+ }
95
+
96
+ if (params.isOutdial) {
97
+ return 'outdial_auto_answer';
98
+ }
99
+
100
+ if (params.enableWxBetterTogether && !params.hasDeviceCallId) {
101
+ return 'extension_non_wxApp_offer';
102
+ }
103
+
104
+ return 'extension_non_wxApp_offer';
105
+ }
106
+
107
+ export function logWxAppOfferDecision(params: {
108
+ interactionId: string;
109
+ acceptVisible: boolean;
110
+ acceptEnabled: boolean;
111
+ acceptReason: WxAppAcceptReason;
112
+ wxAppParticipantDeviceType?: string;
113
+ hasDeviceCallId: boolean;
114
+ }): void {
115
+ logWxApp('offer decision', 'logWxAppOfferDecision', {
116
+ event: 'offer_decision',
117
+ interactionId: params.interactionId,
118
+ acceptVisible: params.acceptVisible,
119
+ acceptEnabled: params.acceptEnabled,
120
+ acceptReason: params.acceptReason,
121
+ wxAppParticipantDeviceType: params.wxAppParticipantDeviceType,
122
+ hasDeviceCallId: params.hasDeviceCallId,
123
+ });
124
+ }
125
+
126
+ export function logWxAppTelephonyAction(params: {
127
+ action: WxAppTelephonyAction;
128
+ phase: WxAppTelephonyPhase;
129
+ interactionId?: string;
130
+ trackingId?: string;
131
+ httpStatus?: number | string;
132
+ failureReason?: string;
133
+ }): void {
134
+ const payload: WxAppLogData = {
135
+ event: 'telephony_action',
136
+ action: params.action,
137
+ phase: params.phase,
138
+ interactionId: params.interactionId,
139
+ trackingId: params.trackingId,
140
+ httpStatus: params.httpStatus,
141
+ failureReason: params.failureReason,
142
+ };
143
+
144
+ if (params.phase === 'failed') {
145
+ logWxAppError(`telephony ${params.action} ${params.phase}`, 'logWxAppTelephonyAction', payload);
146
+ } else {
147
+ logWxApp(`telephony ${params.action} ${params.phase}`, 'logWxAppTelephonyAction', payload);
148
+ }
149
+ }
150
+
151
+ export function logWxAppValidationFailure(reason: string, interactionId?: string): void {
152
+ logWxAppError('validation failed', 'logWxAppValidationFailure', {
153
+ event: 'validation_failed',
154
+ reason,
155
+ interactionId,
156
+ });
157
+ }
158
+
159
+ export function logWxAppMercuryMuteSync(params: {
160
+ phase: 'received' | 'applied' | 'dropped';
161
+ muted?: boolean;
162
+ callIdSuffix?: string;
163
+ interactionId?: string;
164
+ dropReason?: string;
165
+ }): void {
166
+ logWxApp(`mercury mute sync ${params.phase}`, 'logWxAppMercuryMuteSync', {
167
+ event: 'mercury_mute_sync',
168
+ phase: params.phase,
169
+ muted: params.muted,
170
+ callIdSuffix: params.callIdSuffix,
171
+ interactionId: params.interactionId,
172
+ dropReason: params.dropReason,
173
+ });
174
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Expected Hydra responses when mute backfill races call creation (WXCC-6026).
3
+ * These must not be logged as errors — see task-spec wxApp mute backfill guard.
4
+ */
5
+ export function isWxAppCallNotFoundError(error: unknown): boolean {
6
+ const err = error as {status?: number | string; message?: string};
7
+ if (err?.status === 400 || err?.status === '400') {
8
+ return true;
9
+ }
10
+
11
+ const message = String(err?.message ?? error ?? '');
12
+
13
+ return message.includes('Call not found') || message.includes('101002');
14
+ }