@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.3

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +5 -1
  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 +440 -10
  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/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +176 -20
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +1 -1
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +254 -15
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +117 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +653 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -152,6 +152,24 @@ describe('metrics/behavioral-events', () => {
152
152
  verb: 'fail',
153
153
  });
154
154
 
155
+ expect(
156
+ getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS)
157
+ ).toEqual({
158
+ product,
159
+ agent: 'user',
160
+ target: 'task_conference_participant_drop',
161
+ verb: 'complete',
162
+ });
163
+
164
+ expect(
165
+ getEventTaxonomy(METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED)
166
+ ).toEqual({
167
+ product,
168
+ agent: 'user',
169
+ target: 'task_conference_participant_drop',
170
+ verb: 'fail',
171
+ });
172
+
155
173
  expect(getEventTaxonomy(METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS)).toEqual({
156
174
  product,
157
175
  agent: 'user',
@@ -99,7 +99,8 @@ describe('AddressBook', () => {
99
99
 
100
100
  expect(mockWebex.request).toHaveBeenCalledWith({
101
101
  service: 'wcc-api-gateway',
102
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
102
+ resource:
103
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
103
104
  method: HTTP_METHODS.GET,
104
105
  });
105
106
 
@@ -133,7 +134,8 @@ describe('AddressBook', () => {
133
134
  module: 'AddressBook',
134
135
  method: 'getEntries',
135
136
  data: {
136
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
137
+ resource:
138
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
137
139
  service: 'wcc-api-gateway',
138
140
  },
139
141
  });
@@ -155,7 +157,8 @@ describe('AddressBook', () => {
155
157
 
156
158
  expect(mockWebex.request).toHaveBeenCalledWith({
157
159
  service: 'wcc-api-gateway',
158
- resource: '/organization/test-org-id/v2/address-book/custom-book-id/entry?page=1&pageSize=25&filter=name%3D%3D%22John+Doe%22&attributes=id%2Cname%2Cnumber&search=john',
160
+ resource:
161
+ '/organization/test-org-id/v2/address-book/custom-book-id/entry?page=1&pageSize=25&filter=name%3D%3D%22John+Doe%22&attributes=id%2Cname%2Cnumber&search=john&sort=name%2CASC',
159
162
  method: HTTP_METHODS.GET,
160
163
  });
161
164
 
@@ -175,6 +178,31 @@ describe('AddressBook', () => {
175
178
  );
176
179
  });
177
180
 
181
+ it('should allow callers to override the default backend sorting and bypass the default page cache', async () => {
182
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
183
+
184
+ await addressBookAPI.getEntries({
185
+ page: 0,
186
+ pageSize: 25,
187
+ sortBy: 'name',
188
+ sortOrder: 'desc',
189
+ });
190
+ await addressBookAPI.getEntries({
191
+ page: 0,
192
+ pageSize: 25,
193
+ sortBy: 'name',
194
+ sortOrder: 'desc',
195
+ });
196
+
197
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
198
+ expect(mockWebex.request).toHaveBeenLastCalledWith({
199
+ service: 'wcc-api-gateway',
200
+ resource:
201
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=25&sort=name%2CDESC',
202
+ method: HTTP_METHODS.GET,
203
+ });
204
+ });
205
+
178
206
  it('should handle API errors and track metrics', async () => {
179
207
  (mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
180
208
 
@@ -182,7 +210,8 @@ describe('AddressBook', () => {
182
210
 
183
211
  expect(mockWebex.request).toHaveBeenCalledWith({
184
212
  service: 'wcc-api-gateway',
185
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100',
213
+ resource:
214
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=0&pageSize=100&sort=name%2CASC',
186
215
  method: HTTP_METHODS.GET,
187
216
  });
188
217
 
@@ -316,14 +345,16 @@ describe('AddressBook', () => {
316
345
  expect(result.meta.page).toBe(1);
317
346
  expect(mockWebex.request).toHaveBeenCalledWith({
318
347
  service: 'wcc-api-gateway',
319
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100',
348
+ resource:
349
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
320
350
  method: HTTP_METHODS.GET,
321
351
  });
322
352
  expect(LoggerProxy.info).toHaveBeenCalledWith('Making API request to fetch address book entries', {
323
353
  module: 'AddressBook',
324
354
  method: 'getEntries',
325
355
  data: {
326
- resource: '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100',
356
+ resource:
357
+ '/organization/test-org-id/v2/address-book/test-address-book-id/entry?page=1&pageSize=100&sort=name%2CASC',
327
358
  service: 'wcc-api-gateway',
328
359
  },
329
360
  });
@@ -0,0 +1,223 @@
1
+ import {expect} from '@jest/globals';
2
+ import AnswerCallOnWebexService from '../../../../src/services/AnswerCallOnWebexService';
3
+ import {WebexSDK} from '../../../../src/types';
4
+ import LoggerProxy from '../../../../src/logger-proxy';
5
+
6
+ jest.mock('../../../../src/logger-proxy');
7
+ jest.mock('../../../../src/services/core/WebexRequest', () => ({
8
+ __esModule: true,
9
+ default: {
10
+ getInstance: () => ({uploadLogs: jest.fn()}),
11
+ },
12
+ }));
13
+
14
+ const TELEPHONY_BASE = 'https://api.ciscospark.com';
15
+ const CALLS_URL = `${TELEPHONY_BASE}/telephony/calls`;
16
+
17
+ function makeMockWebex(overrides: Partial<{_serviceUrls: Record<string, string>}> = {}): WebexSDK {
18
+ return {
19
+ request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
20
+ internal: {
21
+ services: {
22
+ _serviceUrls: {hydra: TELEPHONY_BASE, ...overrides._serviceUrls},
23
+ get: jest.fn(),
24
+ },
25
+ },
26
+ } as unknown as WebexSDK;
27
+ }
28
+
29
+ describe('AnswerCallOnWebexService', () => {
30
+ let webex: WebexSDK;
31
+ let service: AnswerCallOnWebexService;
32
+
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ webex = makeMockWebex();
36
+ service = new AnswerCallOnWebexService(webex);
37
+ });
38
+
39
+ describe('getTelephonyBaseUrl resolution', () => {
40
+ it('uses hydra URL from _serviceUrls when available', async () => {
41
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
42
+
43
+ expect(webex.request).toHaveBeenCalledWith(
44
+ expect.objectContaining({uri: `${CALLS_URL}/answer`})
45
+ );
46
+ });
47
+
48
+ it('falls back to services.get("hydra") when _serviceUrls.hydra is missing', async () => {
49
+ const hydraBase = 'https://api.ciscospark.com/v1';
50
+ const get = jest.fn().mockReturnValue(hydraBase);
51
+ webex = {
52
+ request: jest.fn().mockResolvedValue({statusCode: 200, body: {}}),
53
+ internal: {
54
+ services: {
55
+ _serviceUrls: {},
56
+ get,
57
+ },
58
+ },
59
+ } as unknown as WebexSDK;
60
+ service = new AnswerCallOnWebexService(webex);
61
+
62
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
63
+
64
+ expect(get).toHaveBeenCalledWith('hydra');
65
+ expect(webex.request).toHaveBeenCalledWith(
66
+ expect.objectContaining({uri: `${hydraBase}/telephony/calls/answer`})
67
+ );
68
+ });
69
+
70
+ it('throws when hydra URL is unavailable', async () => {
71
+ webex = {
72
+ request: jest.fn(),
73
+ internal: {
74
+ services: {
75
+ _serviceUrls: {},
76
+ get: jest.fn().mockReturnValue(''),
77
+ },
78
+ },
79
+ } as unknown as WebexSDK;
80
+ service = new AnswerCallOnWebexService(webex);
81
+
82
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toBeDefined();
83
+ });
84
+ });
85
+
86
+ describe('answerCall', () => {
87
+ it('POST /answer with callId and endpointId', async () => {
88
+ await service.answerCall({callId: 'call-123', endpointId: 'ep-456'});
89
+
90
+ expect(webex.request).toHaveBeenCalledWith({
91
+ uri: `${CALLS_URL}/answer`,
92
+ method: 'POST',
93
+ body: {callId: 'call-123', endpointId: 'ep-456'},
94
+ addAuthHeader: true,
95
+ });
96
+ });
97
+
98
+ it('includes lineOwnerId when provided', async () => {
99
+ await service.answerCall({callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'});
100
+
101
+ expect(webex.request).toHaveBeenCalledWith(
102
+ expect.objectContaining({body: {callId: 'c1', endpointId: 'e1', lineOwnerId: 'lo-1'}})
103
+ );
104
+ });
105
+
106
+ it('omits lineOwnerId when not provided', async () => {
107
+ await service.answerCall({callId: 'c1', endpointId: 'e1'});
108
+
109
+ const call = (webex.request as jest.Mock).mock.calls[0][0];
110
+ expect(call.body).not.toHaveProperty('lineOwnerId');
111
+ });
112
+
113
+ it('rejects and logs on request failure', async () => {
114
+ const err = {
115
+ statusCode: 400,
116
+ details: {
117
+ trackingId: 'track-wxapp-1',
118
+ data: {reason: 'TELEPHONY_ERROR'},
119
+ },
120
+ };
121
+ (webex.request as jest.Mock).mockRejectedValue(err);
122
+
123
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toMatchObject({
124
+ isWxAppTelephonyError: true,
125
+ message: 'TELEPHONY_ERROR',
126
+ trackingId: 'track-wxapp-1',
127
+ status: 400,
128
+ });
129
+ expect(LoggerProxy.error).toHaveBeenCalled();
130
+ });
131
+ });
132
+
133
+ describe('rejectCall', () => {
134
+ it('POST /reject with callId', async () => {
135
+ await service.rejectCall({callId: 'call-123'});
136
+
137
+ expect(webex.request).toHaveBeenCalledWith({
138
+ uri: `${CALLS_URL}/reject`,
139
+ method: 'POST',
140
+ body: {callId: 'call-123'},
141
+ addAuthHeader: true,
142
+ });
143
+ });
144
+ });
145
+
146
+ describe('muteCall', () => {
147
+ it('POST /mute with callId', async () => {
148
+ await service.muteCall({callId: 'call-123'});
149
+
150
+ expect(webex.request).toHaveBeenCalledWith(
151
+ expect.objectContaining({uri: `${CALLS_URL}/mute`, body: {callId: 'call-123'}})
152
+ );
153
+ });
154
+ });
155
+
156
+ describe('unmuteCall', () => {
157
+ it('POST /unmute with callId', async () => {
158
+ await service.unmuteCall({callId: 'call-123'});
159
+
160
+ expect(webex.request).toHaveBeenCalledWith(
161
+ expect.objectContaining({uri: `${CALLS_URL}/unmute`, body: {callId: 'call-123'}})
162
+ );
163
+ });
164
+ });
165
+
166
+ describe('transmitDtmf', () => {
167
+ it('POST /transmitDtmf with callId and dtmf', async () => {
168
+ await service.transmitDtmf({callId: 'call-123', dtmf: '5'});
169
+
170
+ expect(webex.request).toHaveBeenCalledWith({
171
+ uri: `${CALLS_URL}/transmitDtmf`,
172
+ method: 'POST',
173
+ body: {callId: 'call-123', dtmf: '5'},
174
+ addAuthHeader: true,
175
+ });
176
+ });
177
+
178
+ it('includes lineOwnerId in DTMF request when provided', async () => {
179
+ await service.transmitDtmf({callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'});
180
+
181
+ expect(webex.request).toHaveBeenCalledWith(
182
+ expect.objectContaining({body: {callId: 'c1', dtmf: '9', lineOwnerId: 'lo-1'}})
183
+ );
184
+ });
185
+ });
186
+
187
+ describe('getCallDetails', () => {
188
+ it('GET /telephony/calls/{callId}', async () => {
189
+ (webex.request as jest.Mock).mockResolvedValue({body: {muted: true}});
190
+
191
+ const result = await service.getCallDetails({callId: 'call-123'});
192
+
193
+ expect(webex.request).toHaveBeenCalledWith({
194
+ uri: `${CALLS_URL}/call-123`,
195
+ method: 'GET',
196
+ addAuthHeader: true,
197
+ });
198
+ expect(result).toEqual({muted: true});
199
+ });
200
+
201
+ it('includes lineOwnerId query param when provided', async () => {
202
+ await service.getCallDetails({callId: 'call-123', lineOwnerId: 'lo-1'});
203
+
204
+ expect(webex.request).toHaveBeenCalledWith(
205
+ expect.objectContaining({uri: `${CALLS_URL}/call-123?lineOwnerId=lo-1`})
206
+ );
207
+ });
208
+
209
+ it('rejects and logs on request failure', async () => {
210
+ (webex.request as jest.Mock).mockRejectedValue(new Error('network fail'));
211
+
212
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
213
+ expect(LoggerProxy.error).toHaveBeenCalled();
214
+ });
215
+
216
+ it('rejects without error log for expected call-not-found during mute backfill race', async () => {
217
+ (webex.request as jest.Mock).mockRejectedValue({status: 400, message: 'Call not found'});
218
+
219
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
220
+ expect(LoggerProxy.error).not.toHaveBeenCalled();
221
+ });
222
+ });
223
+ });
@@ -1,5 +1,5 @@
1
1
  import EntryPoint from '../../../../src/services/EntryPoint';
2
- import {HTTP_METHODS, WebexSDK, IHttpResponse} from '../../../../src/types';
2
+ import {HTTP_METHODS, WebexSDK, IHttpResponse, EntryPointRecord} from '../../../../src/types';
3
3
  import {METRIC_EVENT_NAMES} from '../../../../src/metrics/constants';
4
4
  import WebexRequest from '../../../../src/services/core/WebexRequest';
5
5
  import MetricsManager from '../../../../src/metrics/MetricsManager';
@@ -50,30 +50,38 @@ describe('EntryPoint', () => {
50
50
  });
51
51
 
52
52
  describe('getEntryPoints', () => {
53
- const mockEntryPoints = [
53
+ const mockDialNumberMappings = [
54
54
  {
55
- id: 'entry1',
56
- name: 'Test Entry Point 1',
57
- type: 'voice',
58
- isActive: true,
59
- orgId: 'test-org-id',
55
+ id: 'dial-number-1',
56
+ dialledNumber: '+1-555-0101',
57
+ entryPointId: 'entry1',
58
+ entryPointName: 'Test Entry Point 1',
60
59
  },
61
60
  {
62
- id: 'entry2',
63
- name: 'Test Entry Point 2',
64
- type: 'chat',
65
- isActive: true,
66
- orgId: 'test-org-id',
61
+ id: 'dial-number-2',
62
+ dialledNumber: '+1-555-0102',
63
+ entryPointId: 'entry2',
64
+ entryPointName: 'Test Entry Point 2',
67
65
  },
68
66
  ];
67
+ const expectedEntryPoints: EntryPointRecord[] = [
68
+ {id: 'entry1', name: 'Test Entry Point 1', number: '+1-555-0101'},
69
+ {id: 'entry2', name: 'Test Entry Point 2', number: '+1-555-0102'},
70
+ ];
71
+ const defaultResource =
72
+ '/organization/test-org-id/v3/dial-number?page=0&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true';
73
+ const requestHeaders = {
74
+ 'X-ORGANIZATION-ID': 'test-org-id',
75
+ 'x-ignore-internal-data': 'false',
76
+ };
69
77
 
70
78
  const mockResponse: IHttpResponse = {
71
79
  statusCode: 200,
72
80
  method: 'GET',
73
- url: '/organization/test-org-id/v2/entry-point',
81
+ url: '/organization/test-org-id/v3/dial-number',
74
82
  headers: {} as any,
75
83
  body: {
76
- data: mockEntryPoints,
84
+ data: mockDialNumberMappings,
77
85
  meta: {
78
86
  page: 0,
79
87
  pageSize: 100,
@@ -99,11 +107,14 @@ describe('EntryPoint', () => {
99
107
 
100
108
  expect(mockWebex.request).toHaveBeenCalledWith({
101
109
  service: 'wcc-api-gateway',
102
- resource: '/organization/test-org-id/v2/entry-point?page=0&pageSize=100&sortOrder=asc',
110
+ resource: defaultResource,
103
111
  method: HTTP_METHODS.GET,
112
+ body: undefined,
113
+ headers: requestHeaders,
104
114
  });
105
115
 
106
- expect(result).toEqual(mockResponse.body);
116
+ expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
117
+ expect(result.data[0].number).toBe('+1-555-0101');
107
118
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith(METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS);
108
119
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
109
120
  METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
@@ -122,7 +133,7 @@ describe('EntryPoint', () => {
122
133
  {module: 'EntryPoint', method: 'getEntryPoints'}
123
134
  );
124
135
  expect(LoggerProxy.log).toHaveBeenCalledWith(
125
- `Making API request to fetch entry points - resource: /organization/test-org-id/v2/entry-point?page=0&pageSize=100&sortOrder=asc, service: wcc-api-gateway`,
136
+ `Making API request to fetch entry points - resource: ${defaultResource}, service: wcc-api-gateway`,
126
137
  {module: 'EntryPoint', method: 'getEntryPoints'}
127
138
  );
128
139
  });
@@ -134,7 +145,7 @@ describe('EntryPoint', () => {
134
145
  page: 1,
135
146
  pageSize: 25,
136
147
  search: 'test',
137
- filter: 'type=="voice"',
148
+ filter: 'entryPointId=="entry1"',
138
149
  attributes: 'id,name',
139
150
  sortBy: 'name',
140
151
  sortOrder: 'desc' as const,
@@ -144,8 +155,11 @@ describe('EntryPoint', () => {
144
155
 
145
156
  expect(mockWebex.request).toHaveBeenCalledWith({
146
157
  service: 'wcc-api-gateway',
147
- resource: '/organization/test-org-id/v2/entry-point?page=1&pageSize=25&sortOrder=desc&search=test&filter=type%3D%3D%22voice%22&attributes=id%2Cname&sortBy=name',
158
+ resource:
159
+ '/organization/test-org-id/v3/dial-number?page=1&pageSize=25&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName%2Cname&search=test&filter=entryPointId%3D%3D%22entry1%22&sort=entryPointName%2CDESC&desktopProfileFilter=true&includeEntryPointName=true',
148
160
  method: HTTP_METHODS.GET,
161
+ body: undefined,
162
+ headers: requestHeaders,
149
163
  });
150
164
 
151
165
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
@@ -162,6 +176,41 @@ describe('EntryPoint', () => {
162
176
  );
163
177
  });
164
178
 
179
+ it('should allow existing parameters to override the default entry-point mapping policy', async () => {
180
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
181
+
182
+ const result = await entryPointAPI.getEntryPoints({
183
+ page: 1,
184
+ pageSize: 25,
185
+ search: 'sales',
186
+ filter: 'entryPointId==entry1',
187
+ });
188
+
189
+ expect(mockWebex.request).toHaveBeenCalledWith({
190
+ service: 'wcc-api-gateway',
191
+ resource:
192
+ '/organization/test-org-id/v3/dial-number?page=1&pageSize=25&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&search=sales&filter=entryPointId%3D%3Dentry1&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true',
193
+ method: HTTP_METHODS.GET,
194
+ body: undefined,
195
+ headers: requestHeaders,
196
+ });
197
+ expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
198
+ });
199
+
200
+ it('should issue every profile-scoped request directly', async () => {
201
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
202
+
203
+ await entryPointAPI.getEntryPoints();
204
+ await entryPointAPI.getEntryPoints();
205
+
206
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
207
+ expect(mockWebex.request).toHaveBeenLastCalledWith(
208
+ expect.objectContaining({
209
+ resource: expect.stringContaining('desktopProfileFilter=true'),
210
+ })
211
+ );
212
+ });
213
+
165
214
  it('should handle API errors and track metrics', async () => {
166
215
  (mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
167
216
 
@@ -169,8 +218,10 @@ describe('EntryPoint', () => {
169
218
 
170
219
  expect(mockWebex.request).toHaveBeenCalledWith({
171
220
  service: 'wcc-api-gateway',
172
- resource: '/organization/test-org-id/v2/entry-point?page=0&pageSize=100&sortOrder=asc',
221
+ resource: defaultResource,
173
222
  method: HTTP_METHODS.GET,
223
+ body: undefined,
224
+ headers: requestHeaders,
174
225
  });
175
226
 
176
227
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
@@ -187,22 +238,24 @@ describe('EntryPoint', () => {
187
238
  expect(LoggerProxy.error).toHaveBeenCalled();
188
239
  });
189
240
 
190
-
191
-
192
- it('should not track metrics for subsequent pages in simple pagination', async () => {
241
+ it('should not track success metrics for subsequent pages under the default mapping policy', async () => {
193
242
  (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
194
243
 
195
244
  const result = await entryPointAPI.getEntryPoints({page: 2});
196
- expect(result).toEqual(mockResponse.body);
245
+ expect(result).toEqual({...mockResponse.body, data: expectedEntryPoints});
197
246
 
198
- expect(mockMetricsManager.trackEvent).not.toHaveBeenCalled();
247
+ expect(mockMetricsManager.trackEvent).not.toHaveBeenCalledWith(
248
+ METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
249
+ expect.anything(),
250
+ expect.anything()
251
+ );
199
252
  });
200
253
 
201
254
  it('should track metrics for search requests on any page', async () => {
202
255
  (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
203
256
 
204
257
  const result2 = await entryPointAPI.getEntryPoints({page: 2, search: 'test'});
205
- expect(result2).toEqual(mockResponse.body);
258
+ expect(result2).toEqual({...mockResponse.body, data: expectedEntryPoints});
206
259
 
207
260
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
208
261
  METRIC_EVENT_NAMES.ENTRYPOINT_FETCH_SUCCESS,
@@ -218,15 +271,11 @@ describe('EntryPoint', () => {
218
271
  );
219
272
  });
220
273
 
221
- it('should call API when requested page is not cached (cache miss)', async () => {
222
- (mockWebex.request as jest.Mock).mockResolvedValueOnce(mockResponse);
223
-
224
- await entryPointAPI.getEntryPoints({page: 0});
225
-
226
- (mockWebex.request as jest.Mock).mockResolvedValueOnce({
274
+ it('should request the specified page', async () => {
275
+ (mockWebex.request as jest.Mock).mockResolvedValue({
227
276
  ...mockResponse,
228
277
  body: {
229
- data: mockEntryPoints,
278
+ data: mockDialNumberMappings,
230
279
  meta: {
231
280
  page: 1,
232
281
  pageSize: 100,
@@ -237,21 +286,19 @@ describe('EntryPoint', () => {
237
286
  },
238
287
  });
239
288
 
240
- const callsBefore = (mockWebex.request as jest.Mock).mock.calls.length;
241
- (LoggerProxy.log as jest.Mock).mockClear();
242
-
243
289
  const result = await entryPointAPI.getEntryPoints({page: 1});
244
- const callsAfter = (mockWebex.request as jest.Mock).mock.calls.length;
245
290
 
246
- expect(callsAfter).toBe(callsBefore + 1);
247
291
  expect(result.meta.page).toBe(1);
248
292
  expect(mockWebex.request).toHaveBeenCalledWith({
249
293
  service: 'wcc-api-gateway',
250
- resource: '/organization/test-org-id/v2/entry-point?page=1&pageSize=100&sortOrder=asc',
294
+ resource:
295
+ '/organization/test-org-id/v3/dial-number?page=1&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true',
251
296
  method: HTTP_METHODS.GET,
297
+ body: undefined,
298
+ headers: requestHeaders,
252
299
  });
253
300
  expect(LoggerProxy.log).toHaveBeenCalledWith(
254
- `Making API request to fetch entry points - resource: /organization/test-org-id/v2/entry-point?page=1&pageSize=100&sortOrder=asc, service: wcc-api-gateway`,
301
+ `Making API request to fetch entry points - resource: /organization/test-org-id/v3/dial-number?page=1&pageSize=100&attributes=id%2CdialledNumber%2CentryPointId%2CentryPointName&sort=entryPointName%2CASC&desktopProfileFilter=true&includeEntryPointName=true, service: wcc-api-gateway`,
255
302
  {module: 'EntryPoint', method: 'getEntryPoints'}
256
303
  );
257
304
  });