@app-connect/core 1.7.35 → 1.7.36

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 (48) hide show
  1. package/connector/mock.js +9 -4
  2. package/connector/proxy/engine.js +3 -2
  3. package/connector/proxy/index.js +2 -2
  4. package/docs/architecture.md +1 -0
  5. package/docs/handlers.md +1 -1
  6. package/docs/models.md +2 -0
  7. package/docs/routes.md +1 -1
  8. package/handlers/disposition.js +3 -2
  9. package/handlers/log.js +16 -6
  10. package/handlers/plugin.js +13 -6
  11. package/index.js +10 -4
  12. package/lib/callLogLookup.js +82 -9
  13. package/lib/migrateCallLogsSchema.js +128 -7
  14. package/models/callLogModel.js +6 -0
  15. package/package.json +1 -1
  16. package/releaseNotes.json +16 -0
  17. package/test/connector/developerPortal.test.js +166 -0
  18. package/test/connector/mock.test.js +131 -0
  19. package/test/connector/proxy/engine.test.js +85 -0
  20. package/test/connector/proxy/index.test.js +246 -0
  21. package/test/connector/proxy/sample.json +1 -0
  22. package/test/handlers/admin.test.js +344 -0
  23. package/test/handlers/appointment.test.js +260 -0
  24. package/test/handlers/calldown.test.js +310 -0
  25. package/test/handlers/disposition.test.js +396 -0
  26. package/test/handlers/log.test.js +324 -1
  27. package/test/handlers/managedOAuth.test.js +262 -0
  28. package/test/handlers/plugin.test.js +305 -0
  29. package/test/handlers/user.test.js +381 -0
  30. package/test/index.test.js +102 -0
  31. package/test/lib/analytics.test.js +146 -0
  32. package/test/lib/authSession.test.js +173 -0
  33. package/test/lib/encode.test.js +59 -0
  34. package/test/lib/errorHandler.test.js +246 -0
  35. package/test/lib/generalErrorMessage.test.js +82 -0
  36. package/test/lib/s3ErrorLogReport.test.js +187 -0
  37. package/test/mcp/mcpHandlerMore.test.js +384 -0
  38. package/test/mcp/tools/appointmentTools.test.js +362 -0
  39. package/test/models/callDownListModel.test.js +125 -0
  40. package/test/models/dynamo/lockSchema.test.js +37 -0
  41. package/test/models/dynamo/noteCacheSchema.test.js +45 -0
  42. package/test/models/llmSessionModel.test.js +91 -0
  43. package/test/models/models.test.js +92 -0
  44. package/test/routes/calldownRoutes.test.js +224 -0
  45. package/test/routes/coreRouterBroadRoutes.test.js +855 -0
  46. package/test/routes/dispositionRoutes.test.js +192 -0
  47. package/test/routes/managedAuthRoutes.test.js +151 -0
  48. package/test/routes/pluginRoutes.test.js +262 -0
package/connector/mock.js CHANGED
@@ -27,19 +27,21 @@ async function deleteUser() {
27
27
  return false;
28
28
  }
29
29
 
30
- async function getCallLog({ sessionIds, extensionNumber }) {
30
+ async function getCallLog({ sessionIds, extensionNumber, hashedExtensionId }) {
31
31
  const sessionIdsArray = sessionIds.split(',');
32
32
  const extensionNumberValue = extensionNumber?.toString() ?? '';
33
+ const hashedExtensionIdValue = hashedExtensionId?.toString() ?? '';
33
34
  const callLogs = await CallLogModel.findAll({
34
35
  where: buildCallLogSessionWhere({
35
36
  sessionIds: sessionIdsArray,
36
37
  extensionNumber: extensionNumberValue,
38
+ hashedExtensionId: hashedExtensionIdValue,
37
39
  }),
38
- order: [['extensionNumber', 'ASC']]
40
+ order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
39
41
  });
40
42
  const logs = [];
41
43
  for (const sId of sessionIdsArray) {
42
- const callLog = findMatchingCallLog(callLogs, sId, extensionNumberValue);
44
+ const callLog = findMatchingCallLog(callLogs, sId, extensionNumberValue, hashedExtensionIdValue);
43
45
  if (!callLog) {
44
46
  logs.push({ sessionId: sId, matched: false });
45
47
  }
@@ -51,12 +53,14 @@ async function getCallLog({ sessionIds, extensionNumber }) {
51
53
  return logs;
52
54
  }
53
55
 
54
- async function createCallLog({ sessionId, extensionNumber }) {
56
+ async function createCallLog({ sessionId, extensionNumber, hashedExtensionId }) {
55
57
  const extensionNumberValue = extensionNumber?.toString() ?? '';
58
+ const hashedExtensionIdValue = hashedExtensionId?.toString() ?? '';
56
59
  let callLog = await CallLogModel.findOne({
57
60
  where: buildCallLogSessionWhere({
58
61
  sessionId,
59
62
  extensionNumber: extensionNumberValue,
63
+ hashedExtensionId: hashedExtensionIdValue,
60
64
  })
61
65
  });
62
66
  if (!callLog) {
@@ -64,6 +68,7 @@ async function createCallLog({ sessionId, extensionNumber }) {
64
68
  id: shortid.generate(),
65
69
  sessionId,
66
70
  extensionNumber: extensionNumberValue,
71
+ hashedExtensionId: hashedExtensionIdValue,
67
72
  userId: 'mockUser'
68
73
  });
69
74
  }
@@ -109,8 +109,8 @@ async function performRequest({ config, opName, inputs, user, authHeader }) {
109
109
  return response;
110
110
  }
111
111
 
112
- function mapFindContactResponse({ config, response }) {
113
- const map = config.operations?.findContact?.responseMapping;
112
+ function mapFindContactResponse({ config, response, opName = 'findContact' }) {
113
+ const map = config.operations?.[opName]?.responseMapping;
114
114
  if (!map) return [];
115
115
  const __ctx = { body: response.data };
116
116
  const list = getByPath(__ctx, map.listPath || 'body') || [];
@@ -123,6 +123,7 @@ function mapFindContactResponse({ config, response }) {
123
123
  type: getByPath(it, itemMap.typePath || 'type') || 'Contact',
124
124
  title: getByPath(it, itemMap.titlePath || 'title') || "",
125
125
  company: getByPath(it, itemMap.companyPath || 'company') || "",
126
+ createdDate: getByPath(it, itemMap.createdDatePath || 'createdDate') || undefined,
126
127
  mostRecentActivityDate: getByPath(it, itemMap.mostRecentActivityDatePath || 'mostRecentActivityDate') || undefined,
127
128
  additionalInfo: getByPath(it, itemMap.additionalInfoPath || 'additionalInfo') || null
128
129
  };
@@ -179,7 +179,7 @@ async function findContact({ user, authHeader, phoneNumber, overridingFormat, is
179
179
  user,
180
180
  authHeader
181
181
  });
182
- const matchedContactInfo = mapFindContactResponse({ config: cfg, response });
182
+ const matchedContactInfo = mapFindContactResponse({ config: cfg, response, opName: 'findContact' });
183
183
  return {
184
184
  successful: true,
185
185
  matchedContactInfo,
@@ -225,7 +225,7 @@ async function findContactWithName({ user, authHeader, name, proxyConfig }) {
225
225
  user,
226
226
  authHeader
227
227
  });
228
- const matchedContactInfo = mapFindContactResponse({ config: cfg, response });
228
+ const matchedContactInfo = mapFindContactResponse({ config: cfg, response, opName: 'findContactWithName' });
229
229
  return {
230
230
  successful: true,
231
231
  matchedContactInfo,
@@ -26,6 +26,7 @@ Its job is to expose a stable HTTP surface, persist shared state, and delegate C
26
26
  - installs an axios interceptor in local-style environments
27
27
  - syncs Sequelize models on startup unless `DISABLE_SYNC_DB_TABLE` or `skipDatabaseInit` disables it
28
28
  - adds the `hashedRcExtensionId` column to `users` if an older schema is missing it
29
+ - migrates `callLogs` to include `hashedExtensionId` in the local call-log identity key when an older schema is missing it
29
30
  - mounts all shared HTTP routes
30
31
  - exposes dev-only mock routes when `IS_PROD === 'false'`
31
32
 
package/docs/handlers.md CHANGED
@@ -59,7 +59,7 @@ This is the heaviest handler in the package.
59
59
 
60
60
  Key responsibilities:
61
61
 
62
- - prevents duplicate call-log creation by checking `CallLogModel` on `sessionId`
62
+ - prevents duplicate call-log creation by checking `CallLogModel` on `sessionId` plus extension identity (`hashedExtensionId` first, with legacy `extensionNumber` fallback)
63
63
  - loads note cache from DynamoDB when `USE_CACHE` and server-side call logging are enabled
64
64
  - runs configured synchronous plugins before creating or updating logs
65
65
  - dispatches configured asynchronous call plugins after call-log create or update succeeds
package/docs/models.md CHANGED
@@ -35,6 +35,8 @@ Stores the mapping between telephony sessions and CRM call logs.
35
35
  | --- | --- |
36
36
  | `id` | Telephony call id |
37
37
  | `sessionId` | Session id; also part of the composite primary key |
38
+ | `extensionNumber` | Legacy RingCentral extension number identity; part of the composite primary key |
39
+ | `hashedExtensionId` | Hashed RingCentral extension id used for current call-log identity; part of the composite primary key |
38
40
  | `platform` | Connector platform |
39
41
  | `thirdPartyLogId` | CRM log id |
40
42
  | `userId` | App Connect user id |
package/docs/routes.md CHANGED
@@ -75,7 +75,7 @@ This page documents the non-MCP HTTP routes defined in `index.js`.
75
75
  | Method | Path | Purpose |
76
76
  | --- | --- | --- |
77
77
  | `POST` | `/callLog/cacheNote` | Stores a temporary note for server-side call logging |
78
- | `GET` | `/callLog` | Looks up existing call logs by session id |
78
+ | `GET` | `/callLog` | Looks up existing call logs by session id plus extension identity |
79
79
  | `POST` | `/callLog` | Creates a CRM call log and stores the local linkage record |
80
80
  | `PATCH` | `/callLog` | Updates an existing CRM call log |
81
81
  | `PUT` | `/callDisposition` | Upserts call disposition data through the connector |
@@ -6,12 +6,13 @@ const { Connector } = require('../models/dynamo/connectorSchema');
6
6
  const { handleApiError } = require('../lib/errorHandler');
7
7
  const { buildCallLogSessionWhere } = require('../lib/callLogLookup');
8
8
 
9
- async function upsertCallDisposition({ platform, userId, sessionId, extensionNumber, dispositions }) {
9
+ async function upsertCallDisposition({ platform, userId, sessionId, extensionNumber, hashedExtensionId, dispositions }) {
10
10
  try {
11
11
  const existingCallLog = await CallLogModel.findOne({
12
12
  where: buildCallLogSessionWhere({
13
13
  sessionId,
14
14
  extensionNumber,
15
+ hashedExtensionId,
15
16
  })
16
17
  });
17
18
  if (!existingCallLog) {
@@ -75,7 +76,7 @@ async function upsertCallDisposition({ platform, userId, sessionId, extensionNum
75
76
  return { successful: !!logId, logId, returnMessage, extraDataTracking };
76
77
  }
77
78
  catch (e) {
78
- return handleApiError(e, platform, 'upsertCallDisposition', { userId, sessionId, extensionNumber, dispositions });
79
+ return handleApiError(e, platform, 'upsertCallDisposition', { userId, sessionId, extensionNumber, hashedExtensionId, dispositions });
79
80
  }
80
81
  }
81
82
 
package/handlers/log.js CHANGED
@@ -20,6 +20,7 @@ const { AccountDataModel } = require('../models/accountDataModel');
20
20
  const pluginCore = require('./plugin');
21
21
  const {
22
22
  getCallLogExtensionNumber,
23
+ getCallLogHashedExtensionId,
23
24
  buildCallLogSessionWhere,
24
25
  findMatchingCallLog,
25
26
  } = require('../lib/callLogLookup');
@@ -73,6 +74,7 @@ function buildAsyncPluginTaskData({ taskId, callbackUrl, pluginId, platform, ope
73
74
  rcAccountId: user.rcAccountId,
74
75
  sessionId: logInfo.sessionId ?? incomingData?.sessionId,
75
76
  extensionNumber: getCallLogExtensionNumber(incomingData),
77
+ hashedExtensionId: getCallLogHashedExtensionId(incomingData),
76
78
  callLogId: existingCallLog?.id ?? logInfo.telephonySessionId ?? logInfo.id,
77
79
  thirdPartyLogId: existingCallLog?.thirdPartyLogId,
78
80
  contactId: existingCallLog?.contactId ?? incomingData?.contactId,
@@ -307,6 +309,7 @@ async function appendAsyncPluginNoteToCallLog({ taskCache, note }) {
307
309
  ...buildCallLogSessionWhere({
308
310
  sessionId: taskData.sessionId,
309
311
  extensionNumber: taskData.extensionNumber,
312
+ hashedExtensionId: taskData.hashedExtensionId,
310
313
  }),
311
314
  platform: taskData.platform,
312
315
  userId: taskData.userId,
@@ -448,12 +451,14 @@ async function handleAsyncPluginCallback({ taskId, body }) {
448
451
  async function createCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
449
452
  try {
450
453
  const extensionNumber = getCallLogExtensionNumber(incomingData);
454
+ const hashedExtensionId = getCallLogHashedExtensionId(incomingData);
451
455
  let existingCallLog = null;
452
456
  try {
453
457
  existingCallLog = await CallLogModel.findOne({
454
458
  where: buildCallLogSessionWhere({
455
459
  sessionId: incomingData.logInfo.sessionId,
456
460
  extensionNumber,
461
+ hashedExtensionId,
457
462
  })
458
463
  });
459
464
  }
@@ -612,6 +617,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
612
617
  id: incomingData.logInfo.telephonySessionId || incomingData.logInfo.id,
613
618
  sessionId: incomingData.logInfo.sessionId,
614
619
  extensionNumber,
620
+ hashedExtensionId,
615
621
  platform,
616
622
  thirdPartyLogId: logId,
617
623
  userId,
@@ -651,7 +657,7 @@ async function createCallLog({ platform, userId, incomingData, hashedAccountId,
651
657
  }
652
658
  }
653
659
 
654
- async function getCallLog({ userId, sessionIds, extensionNumber, platform, requireDetails }) {
660
+ async function getCallLog({ userId, sessionIds, extensionNumber, hashedExtensionId, platform, requireDetails }) {
655
661
  try {
656
662
  let user = await UserModel.findByPk(userId);
657
663
  if (!user || !user.accessToken) {
@@ -706,15 +712,16 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
706
712
  where: buildCallLogSessionWhere({
707
713
  sessionIds: sessionIdsArray,
708
714
  extensionNumber,
715
+ hashedExtensionId,
709
716
  }),
710
- order: [['extensionNumber', 'ASC']]
717
+ order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
711
718
  });
712
719
  for (const sId of sessionIdsArray) {
713
720
  if (sId == 0) {
714
721
  logs.push({ sessionId: sId, matched: false });
715
722
  continue;
716
723
  }
717
- const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
724
+ const callLog = findMatchingCallLog(callLogs, sId, extensionNumber, hashedExtensionId);
718
725
  if (!callLog) {
719
726
  logs.push({ sessionId: sId, matched: false });
720
727
  }
@@ -731,11 +738,12 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
731
738
  where: buildCallLogSessionWhere({
732
739
  sessionIds: sessionIdsArray,
733
740
  extensionNumber,
741
+ hashedExtensionId,
734
742
  }),
735
- order: [['extensionNumber', 'ASC']]
743
+ order: [['hashedExtensionId', 'ASC'], ['extensionNumber', 'ASC']]
736
744
  });
737
745
  for (const sId of sessionIdsArray) {
738
- const callLog = findMatchingCallLog(callLogs, sId, extensionNumber);
746
+ const callLog = findMatchingCallLog(callLogs, sId, extensionNumber, hashedExtensionId);
739
747
  if (!callLog) {
740
748
  logs.push({ sessionId: sId, matched: false });
741
749
  }
@@ -747,19 +755,21 @@ async function getCallLog({ userId, sessionIds, extensionNumber, platform, requi
747
755
  return { successful: true, logs, returnMessage, extraDataTracking };
748
756
  }
749
757
  catch (e) {
750
- return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails, extensionNumber });
758
+ return handleApiError(e, platform, 'getCallLog', { userId, sessionIds, requireDetails, extensionNumber, hashedExtensionId });
751
759
  }
752
760
  }
753
761
 
754
762
  async function updateCallLog({ platform, userId, incomingData, hashedAccountId, isFromSSCL }) {
755
763
  try {
756
764
  const extensionNumber = getCallLogExtensionNumber(incomingData);
765
+ const hashedExtensionId = getCallLogHashedExtensionId(incomingData);
757
766
  let existingCallLog = null;
758
767
  try {
759
768
  existingCallLog = await CallLogModel.findOne({
760
769
  where: buildCallLogSessionWhere({
761
770
  sessionId: incomingData.sessionId,
762
771
  extensionNumber,
772
+ hashedExtensionId,
763
773
  })
764
774
  });
765
775
  }
@@ -48,7 +48,14 @@ async function getPluginLicenseStatus({ rcAccountId, pluginId }) {
48
48
  'Authorization': `Bearer ${accountData.data.jwtToken}`
49
49
  }
50
50
  });
51
- return licenseStatusResponse.data;
51
+ const licenseStatus = licenseStatusResponse.data;
52
+ if (!licenseStatus || !Object.prototype.hasOwnProperty.call(licenseStatus, 'licenseStatus')) {
53
+ return {
54
+ licenseStatus: false,
55
+ licenseStatusDescription: 'Plugin license status unavailable'
56
+ };
57
+ }
58
+ return licenseStatus;
52
59
  }
53
60
 
54
61
  function getRefreshedJwtTokenFromHeaders({ headers }) {
@@ -58,15 +65,15 @@ function getRefreshedJwtTokenFromHeaders({ headers }) {
58
65
  return headers['x-refreshed-jwt-token'] || headers['X-Refreshed-Jwt-Token'] || null;
59
66
  }
60
67
 
61
- async function resolvePluginManifest({ pluginId, pluginAccess, rcAccountId, pluginName }) {
68
+ async function resolvePluginManifest({ pluginId, pluginAccess, ownerRcAccountId, pluginName }) {
62
69
  const manifestFetchers = [];
63
70
  if (pluginAccess === 'public') {
64
71
  manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?type=plugin`);
65
72
  } else if (pluginAccess === 'private' || pluginAccess === 'shared') {
66
- manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${rcAccountId}`);
73
+ manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${ownerRcAccountId}`);
67
74
  } else {
68
75
  manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?type=plugin`);
69
- manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${rcAccountId}`);
76
+ manifestFetchers.push(`${PUBLIC_MANIFEST_BASE}/${pluginId}/manifest?access=internal&type=plugin&accountId=${ownerRcAccountId}`);
70
77
  }
71
78
 
72
79
  let pluginData = null;
@@ -132,8 +139,8 @@ async function persistPluginData({ rcAccountId, pluginId, jwtToken, pluginData =
132
139
  }
133
140
  }
134
141
 
135
- async function registerPluginAccount({ pluginId, rcAccessToken, rcAccountId, pluginAccess, pluginName }) {
136
- const { pluginManifest } = await resolvePluginManifest({ pluginId, pluginAccess, rcAccountId, pluginName });
142
+ async function registerPluginAccount({ pluginId, rcAccessToken, rcAccountId, pluginAccess, pluginName, ownerRcAccountId }) {
143
+ const { pluginManifest } = await resolvePluginManifest({ pluginId, pluginAccess, ownerRcAccountId, pluginName });
137
144
  if (!pluginManifest?.endpointUrl) {
138
145
  throw new Error(`Plugin endpoint URL not found for ${pluginId}`);
139
146
  }
package/index.js CHANGED
@@ -33,6 +33,8 @@ const logger = require('./lib/logger');
33
33
  const { DebugTracer } = require('./lib/debugTracer');
34
34
  const s3ErrorLogReport = require('./lib/s3ErrorLogReport');
35
35
  const pluginCore = require('./handlers/plugin');
36
+ const { sequelize } = require('./models/sequelize');
37
+ const { ensureCallLogsHashedExtensionIdSchema } = require('./lib/migrateCallLogsSchema');
36
38
  const { handleDatabaseError } = require('./lib/errorHandler');
37
39
  const { updateAuthSession } = require('./lib/authSession');
38
40
  const managedAuthCore = require('./handlers/managedAuth');
@@ -71,6 +73,7 @@ async function initDB() {
71
73
  await UserModel.sync();
72
74
  await LlmSessionModel.sync();
73
75
  await CallLogModel.sync();
76
+ await ensureCallLogsHashedExtensionIdSchema(sequelize);
74
77
  await MessageLogModel.sync();
75
78
  await AdminConfigModel.sync();
76
79
  await CacheModel.sync();
@@ -1040,7 +1043,7 @@ function createCoreRouter() {
1040
1043
  if (jwtToken) {
1041
1044
  const unAuthData = jwt.decodeJwt(jwtToken);
1042
1045
  platformName = unAuthData?.platform ?? 'Unknown';
1043
- if(!unAuthData || !unAuthData?.id) {
1046
+ if (!unAuthData || !unAuthData?.id) {
1044
1047
  tracer?.trace('getUserSettings:noToken', {});
1045
1048
  res.status(400).send(tracer ? tracer.wrapResponse('Please go to Settings and authorize CRM platform') : 'Please go to Settings and authorize CRM platform');
1046
1049
  return;
@@ -2036,6 +2039,7 @@ function createCoreRouter() {
2036
2039
  userId,
2037
2040
  sessionIds: req.query.sessionIds,
2038
2041
  extensionNumber: req.query.extensionNumber,
2042
+ hashedExtensionId: req.query.hashedExtensionId,
2039
2043
  platform,
2040
2044
  requireDetails: req.query.requireDetails === 'true'
2041
2045
  });
@@ -2232,6 +2236,7 @@ function createCoreRouter() {
2232
2236
  userId,
2233
2237
  sessionId: req.body.sessionId,
2234
2238
  extensionNumber: req.body.extensionNumber,
2239
+ hashedExtensionId: req.body.hashedExtensionId,
2235
2240
  dispositions: req.body.dispositions,
2236
2241
  additionalSubmission: req.body.additionalSubmission
2237
2242
  });
@@ -2758,7 +2763,7 @@ function createCoreRouter() {
2758
2763
  let success = false;
2759
2764
  const { hashedExtensionId, hashedAccountId, userAgent, ip, author, eventAddedVia } = getAnalyticsVariablesInReqHeaders({ headers: req.headers })
2760
2765
  try {
2761
- const { pluginId, rcAccountId, pluginAccess, pluginName } = req.body || {};
2766
+ const { pluginId, rcAccountId, pluginAccess, pluginName, ownerRcAccountId } = req.body || {};
2762
2767
  const rcAccessToken = req.query?.rcAccessToken;
2763
2768
  if (!pluginId || !rcAccountId) {
2764
2769
  res.status(400).send(tracer ? tracer.wrapResponse({ successful: false, returnMessage: 'pluginId and rcAccountId are required' }) : { successful: false, returnMessage: 'pluginId and rcAccountId are required' });
@@ -2782,6 +2787,7 @@ function createCoreRouter() {
2782
2787
  pluginId,
2783
2788
  rcAccessToken,
2784
2789
  rcAccountId: rcAccountId?.toString(),
2790
+ ownerRcAccountId: ownerRcAccountId?.toString(),
2785
2791
  pluginAccess,
2786
2792
  pluginName,
2787
2793
  });
@@ -2920,7 +2926,7 @@ function createCoreRouter() {
2920
2926
  router.get('/mockCallLog', async function (req, res) {
2921
2927
  const secretKey = req.query.secretKey;
2922
2928
  if (secretKey === process.env.APP_SERVER_SECRET_KEY) {
2923
- const callLogs = await mock.getCallLog({ sessionIds: req.query.sessionIds, extensionNumber: req.query.extensionNumber });
2929
+ const callLogs = await mock.getCallLog({ sessionIds: req.query.sessionIds, extensionNumber: req.query.extensionNumber, hashedExtensionId: req.query.hashedExtensionId });
2924
2930
  res.status(200).send(callLogs);
2925
2931
  }
2926
2932
  else {
@@ -2930,7 +2936,7 @@ function createCoreRouter() {
2930
2936
  router.post('/mockCallLog', async function (req, res) {
2931
2937
  const secretKey = req.query.secretKey;
2932
2938
  if (secretKey === process.env.APP_SERVER_SECRET_KEY) {
2933
- await mock.createCallLog({ sessionId: req.body.sessionId, extensionNumber: req.body.extensionNumber });
2939
+ await mock.createCallLog({ sessionId: req.body.sessionId, extensionNumber: req.body.extensionNumber, hashedExtensionId: req.body.hashedExtensionId });
2934
2940
  res.status(200).send('Mock call log created');
2935
2941
  }
2936
2942
  else {
@@ -1,10 +1,39 @@
1
1
  const Op = require('sequelize').Op;
2
+ const { getHashValue } = require('./util');
3
+
4
+ function stringOrEmpty(value) {
5
+ return value == null ? '' : value.toString();
6
+ }
2
7
 
3
8
  function getCallLogExtensionNumber(incomingData) {
4
- return (incomingData?.extensionNumber ?? incomingData?.logInfo?.extensionNumber)?.toString() ?? '';
9
+ return stringOrEmpty(incomingData?.extensionNumber ?? incomingData?.logInfo?.extensionNumber);
10
+ }
11
+
12
+ function getCallLogHashedExtensionId(incomingData, hashKey = process.env.HASH_KEY) {
13
+ const hashedExtensionId = incomingData?.hashedExtensionId ?? incomingData?.logInfo?.hashedExtensionId;
14
+ if (hashedExtensionId) {
15
+ return hashedExtensionId.toString();
16
+ }
17
+
18
+ const rcExtensionId = incomingData?.rcExtensionId ?? incomingData?.logInfo?.rcExtensionId;
19
+ if (rcExtensionId && hashKey) {
20
+ return getHashValue(rcExtensionId.toString(), hashKey);
21
+ }
22
+
23
+ return '';
24
+ }
25
+
26
+ function buildLegacyIdentityWhere(extensionNumberValue) {
27
+ return {
28
+ extensionNumber: extensionNumberValue,
29
+ [Op.or]: [
30
+ { hashedExtensionId: '' },
31
+ { hashedExtensionId: null },
32
+ ],
33
+ };
5
34
  }
6
35
 
7
- function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber }) {
36
+ function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber, hashedExtensionId }) {
8
37
  const where = {};
9
38
  if (sessionIds) {
10
39
  where.sessionId = {
@@ -14,21 +43,65 @@ function buildCallLogSessionWhere({ sessionId, sessionIds, extensionNumber }) {
14
43
  else {
15
44
  where.sessionId = sessionId;
16
45
  }
17
- const extensionNumberValue = extensionNumber?.toString() ?? '';
18
- if (extensionNumberValue) {
46
+
47
+ const extensionNumberValue = stringOrEmpty(extensionNumber);
48
+ const hashedExtensionIdValue = stringOrEmpty(hashedExtensionId);
49
+
50
+ if (hashedExtensionIdValue) {
51
+ const identityWhere = [
52
+ { hashedExtensionId: hashedExtensionIdValue },
53
+ ];
54
+ if (extensionNumberValue) {
55
+ identityWhere.push(buildLegacyIdentityWhere(extensionNumberValue));
56
+ }
57
+ identityWhere.push(buildLegacyIdentityWhere(''));
58
+ where[Op.or] = identityWhere;
59
+ }
60
+ else if (extensionNumberValue) {
19
61
  where.extensionNumber = extensionNumberValue;
20
62
  }
21
63
  return where;
22
64
  }
23
65
 
24
- function findMatchingCallLog(callLogs, sessionId, extensionNumber) {
25
- const extensionNumberValue = extensionNumber?.toString() ?? '';
26
- return callLogs.find(callLog => (
27
- callLog.sessionId === sessionId &&
28
- (!extensionNumberValue || (callLog.extensionNumber?.toString() ?? '') === extensionNumberValue)
66
+ function hasEmptyHashedExtensionId(callLog) {
67
+ return !stringOrEmpty(callLog.hashedExtensionId);
68
+ }
69
+
70
+ function findMatchingCallLog(callLogs, sessionId, extensionNumber, hashedExtensionId) {
71
+ const extensionNumberValue = stringOrEmpty(extensionNumber);
72
+ const hashedExtensionIdValue = stringOrEmpty(hashedExtensionId);
73
+ const sessionLogs = callLogs.filter(callLog => callLog.sessionId === sessionId);
74
+
75
+ if (hashedExtensionIdValue) {
76
+ const exactHashedLog = sessionLogs.find(callLog => (
77
+ stringOrEmpty(callLog.hashedExtensionId) === hashedExtensionIdValue
78
+ ));
79
+ if (exactHashedLog) {
80
+ return exactHashedLog;
81
+ }
82
+
83
+ if (extensionNumberValue) {
84
+ const legacyExtensionLog = sessionLogs.find(callLog => (
85
+ hasEmptyHashedExtensionId(callLog) &&
86
+ stringOrEmpty(callLog.extensionNumber) === extensionNumberValue
87
+ ));
88
+ if (legacyExtensionLog) {
89
+ return legacyExtensionLog;
90
+ }
91
+ }
92
+
93
+ return sessionLogs.find(callLog => (
94
+ hasEmptyHashedExtensionId(callLog) &&
95
+ !stringOrEmpty(callLog.extensionNumber)
96
+ ));
97
+ }
98
+
99
+ return sessionLogs.find(callLog => (
100
+ !extensionNumberValue || stringOrEmpty(callLog.extensionNumber) === extensionNumberValue
29
101
  ));
30
102
  }
31
103
 
32
104
  exports.getCallLogExtensionNumber = getCallLogExtensionNumber;
105
+ exports.getCallLogHashedExtensionId = getCallLogHashedExtensionId;
33
106
  exports.buildCallLogSessionWhere = buildCallLogSessionWhere;
34
107
  exports.findMatchingCallLog = findMatchingCallLog;