collabSupport/services/cdrFeedParser.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

132 lines
3.7 KiB
JavaScript

// services/cdrFeedParser.js
// Parse cdr_feed JSON payloads (field names vary by Webex release).
function normKey(key) {
return String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
}
function indexCdrRecord(item) {
if (!item || typeof item !== 'object') return {};
const idx = {};
for (const [k, v] of Object.entries(item)) {
idx[normKey(k)] = v;
}
return idx;
}
/** Read a CDR field by any known alias (camelCase or report column label). */
export function cdrField(item, ...aliases) {
if (!item) return null;
const idx = indexCdrRecord(item);
for (const alias of aliases) {
const v = idx[normKey(alias)];
if (v != null && v !== '') return v;
}
return null;
}
export function cdrRecordId(item) {
return cdrField(
item,
'reportId', 'Report ID', 'reportID',
'callRecordingId',
);
}
export function cdrStartTime(item) {
return cdrField(
item,
'startTime', 'start', 'callStartTime',
'Start time', 'Start Time',
'answerTime', 'Answer time', 'Answer Time',
);
}
export function cdrDurationSeconds(item) {
const raw = cdrField(
item,
'durationSeconds', 'duration', 'callDuration', 'talkDuration', 'length',
'Duration',
);
return Number(raw || 0);
}
export function cdrDirectionValue(item) {
return String(cdrField(
item,
'direction', 'callDirection', 'callType', 'directionIndicator',
'Direction', 'Call direction', 'Call Direction',
) || '').toUpperCase();
}
export function cdrDispositionValue(item) {
return String(cdrField(
item,
'status', 'result', 'callResult', 'callOutcome', 'callStatus',
'Disposition', 'Answer indicator', 'Call outcome',
) || 'unknown');
}
export function cdrCallingNumber(item) {
return cdrField(
item,
'callingNumber', 'callingParty', 'callingName',
'Calling number', 'Calling Number',
'User number', 'User Number',
);
}
export function cdrCalledNumber(item) {
return cdrField(
item,
'calledNumber', 'calledParty', 'calledName',
'Called number', 'Called Number',
);
}
export function cdrDedupKey(item) {
const id = cdrRecordId(item);
if (id) return `id:${id}`;
return [
cdrStartTime(item) || '',
cdrCallingNumber(item) || '',
cdrCalledNumber(item) || '',
cdrDurationSeconds(item) || '',
].join('|');
}
export function extractCdrFeedList(data) {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.items)) return data.items;
if (data.items && typeof data.items === 'object' && data.items !== null) {
const keys = Object.keys(data.items).filter((k) => /^\d+$/.test(k)).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
if (keys.length > 0) return keys.map((k) => data.items[k]);
}
const topKeys = Object.keys(data).filter((k) => /^\d+$/.test(k)).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
if (topKeys.length > 0) return topKeys.map((k) => data[k]);
for (const key of ['records', 'cdrRecords', 'callHistory', 'calls']) {
if (Array.isArray(data[key])) return data[key];
}
if (data.data) {
if (Array.isArray(data.data)) return data.data;
if (Array.isArray(data.data.items)) return data.data.items;
}
for (const v of Object.values(data)) {
if (Array.isArray(v)) return v;
}
return [];
}
export function describeCdrFeedPayload(data) {
if (!data) return 'empty body';
if (Array.isArray(data)) return `array[${data.length}]`;
const keys = Object.keys(data);
const parts = [`keys=${keys.slice(0, 12).join(',')}`];
for (const k of ['items', 'records', 'cdrRecords', 'next', 'data']) {
const v = data[k];
if (Array.isArray(v)) parts.push(`${k}.length=${v.length}`);
else if (v && typeof v === 'object') parts.push(`${k}.keys=${Object.keys(v).slice(0, 6).join(',')}`);
}
return parts.join(' ');
}