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>
132 lines
3.7 KiB
JavaScript
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(' ');
|
|
}
|