collabSupport/services/callTest/locationResolver.js
jmcqueen 1eaabbcee1 Add /voicereport store digest and harden Webex CDR feed handling.
Groups cdr_feed legs by Correlation ID for call-level summaries, fixes report-column field parsing and Docker proxy routing, and adds /calltest post-call Twilio and CDR enrichment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 13:43:11 -04:00

116 lines
3.1 KiB
JavaScript

// services/callTest/locationResolver.js
// Resolve Webex Calling location from a dialed E.164 (store main / AA DID).
import { logger } from '../../utils/logger.js';
import { normalizeE164 } from './config.js';
const LOG_SCOPE = 'calltest:location';
const CACHE_TTL_MS = 60 * 60 * 1000;
let _phoneIndex = null;
let _phoneIndexAt = 0;
async function getWebex() {
const mod = await import('../../integrations/webex/WebexClient.js');
return mod.default;
}
function parseLinkNext(linkHeader) {
if (!linkHeader || typeof linkHeader !== 'string') return null;
for (const part of linkHeader.split(',')) {
const m = part.match(/<([^>]+)>\s*;\s*rel\s*=\s*"?next"?/i);
if (m) return m[1];
}
return null;
}
function phoneDigits(value) {
const d = String(value || '').replace(/\D/g, '');
if (d.length === 10) return `1${d}`;
if (d.length === 11 && d.startsWith('1')) return d;
return d;
}
async function fetchAllNumbers() {
const webex = await getWebex();
const all = [];
let nextUrl = null;
let data;
let headers;
({ data, headers } = await webex.requestRaw('GET', 'telephony/config/numbers', null, { max: 1000 }));
const batch = data?.phoneNumbers || [];
all.push(...batch);
nextUrl = parseLinkNext(headers?.link || headers?.Link);
while (nextUrl) {
({ data, headers } = await webex.requestRaw('GET', nextUrl));
all.push(...(data?.phoneNumbers || []));
nextUrl = parseLinkNext(headers?.link || headers?.Link);
}
return all;
}
async function buildPhoneIndex() {
const now = Date.now();
if (_phoneIndex && now - _phoneIndexAt < CACHE_TTL_MS) {
return _phoneIndex;
}
logger(LOG_SCOPE, 'Building location phone index from telephony/config/numbers', 'debug');
const numbers = await fetchAllNumbers();
const index = new Map();
for (const n of numbers) {
const raw = n.phoneNumber || n.number || n.value;
const loc = n.location;
if (!raw || !loc?.id) continue;
const entry = {
locationId: loc.id,
locationName: loc.name || null,
phoneNumber: raw,
};
const keys = new Set([phoneDigits(raw), phoneDigits(normalizeE164(raw) || raw)]);
for (const k of keys) {
if (k) index.set(k, entry);
}
}
_phoneIndex = index;
_phoneIndexAt = now;
logger(LOG_SCOPE, `Phone index built: ${index.size} keys from ${numbers.length} numbers`, 'debug');
return index;
}
/**
* Find Webex location for a dialed E.164 (e.g. store main on /calltest dial).
* @returns {Promise<{locationId: string, locationName: string, phoneNumber: string}|null>}
*/
export async function resolveLocationForDialNumber(dialNumber) {
const e164 = normalizeE164(dialNumber);
if (!e164) return null;
try {
const index = await buildPhoneIndex();
const key = phoneDigits(e164);
const hit = index.get(key);
if (!hit?.locationName) return null;
return {
locationId: hit.locationId,
locationName: hit.locationName,
phoneNumber: hit.phoneNumber,
};
} catch (err) {
logger(LOG_SCOPE, `resolveLocationForDialNumber failed: ${err.message}`, 'warn');
return null;
}
}
export function _clearLocationCacheForTests() {
_phoneIndex = null;
_phoneIndexAt = 0;
}