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>
116 lines
3.1 KiB
JavaScript
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;
|
|
}
|