collabSupport/services/callReport/joinCallQuality.js
jmcqueen a25fc08fe2 Rename /voicereport to /callreport with scoped user and phone targets.
Add store/email/phone filtering, richer call-line formatting, CDR feed pagination and queueing, and split Jira poller enrichment into testable modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:10:03 -04:00

143 lines
4.5 KiB
JavaScript

// services/callReport/joinCallQuality.js
// Join grouped CDR calls with Prisma WAN buckets.
import { isWithinWindow } from './businessWindow.js';
import { cdrStartTime } from '../cdrFeedParser.js';
import {
filterAbnormalCalls,
groupCallsByBucket,
groupCdrIntoCalls,
summarizeCalls,
} from './groupCdrCalls.js';
import { filterCallsByTarget, rebucketCalls } from './filterCallsByTarget.js';
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
function shouldAttachWan(call) {
if (call.direction === 'inbound') return Boolean(call.reachedPhone);
if (call.direction === 'outbound') return Boolean(call.connected || call.reachedPhone);
return false;
}
function intervalMsFromSeries(summary) {
if (summary?.interval === '5min') return DEFAULT_INTERVAL_MS;
return DEFAULT_INTERVAL_MS;
}
function valueAtTime(series, time) {
if (!series || time == null) return null;
if (series.timedPoints?.length) {
const hit = series.timedPoints.find((p) => p.time === time);
return hit?.value ?? null;
}
return null;
}
function overlappingBucketTimes(series, startMs, endMs, intervalMs, winStartMs) {
const times = new Set();
if (series?.timedPoints?.length) {
for (const p of series.timedPoints) {
if (p.value == null || !p.time) continue;
const t = new Date(p.time).getTime();
if (!Number.isFinite(t)) continue;
if (t < endMs && (t + intervalMs) > startMs) times.add(p.time);
}
return times;
}
// Fallback for tests / legacy summaries without timedPoints.
const mosVals = series?.values || [];
if (!mosVals.length || !Number.isFinite(winStartMs)) return times;
const idx0 = Math.max(0, Math.floor((startMs - winStartMs) / intervalMs));
const idx1 = Math.max(idx0, Math.ceil((endMs - winStartMs) / intervalMs));
for (let i = idx0; i <= idx1 && i < mosVals.length; i++) {
if (mosVals[i] != null) times.add(`__idx:${i}`);
}
return times;
}
function worstFromTimedOverlap(appAudio, startMs, endMs, winStartMs) {
const mosSeries = appAudio?.mos;
if (!mosSeries) return null;
const intervalMs = intervalMsFromSeries(mosSeries);
const bucketTimes = overlappingBucketTimes(mosSeries, startMs, endMs, intervalMs, winStartMs);
if (!bucketTimes.size) return null;
let worstMos = null;
let worstLoss = null;
let worstJitter = null;
for (const time of bucketTimes) {
let m;
let l;
let j;
if (time.startsWith('__idx:')) {
const i = Number(time.slice(6));
m = mosSeries.values?.[i];
l = appAudio.loss?.values?.[i];
j = appAudio.jitter?.values?.[i];
} else {
m = valueAtTime(mosSeries, time);
l = valueAtTime(appAudio.loss, time);
j = valueAtTime(appAudio.jitter, time);
}
if (typeof m === 'number' && (worstMos == null || m < worstMos)) worstMos = m;
if (typeof l === 'number' && (worstLoss == null || l > worstLoss)) worstLoss = l;
if (typeof j === 'number' && (worstJitter == null || j > worstJitter)) worstJitter = j;
}
if (worstMos == null && worstLoss == null && worstJitter == null) return null;
return {
siteLevelApprox: true,
mos: worstMos,
loss: worstLoss,
jitter: worstJitter,
};
}
/**
* Find worst Prisma 5-min bucket overlapping [start, end].
*/
export function worstPrismaBucketForCall(call, appAudio, window) {
if (!appAudio?.mos?.values?.length && !appAudio?.mos?.timedPoints?.length) return null;
const startMs = call.start ? new Date(call.start).getTime() : NaN;
const endMs = Number.isFinite(startMs)
? startMs + (call.duration || 0) * 1000
: NaN;
if (!Number.isFinite(startMs)) return null;
const winStartMs = window?.startTime ? new Date(window.startTime).getTime() : startMs;
return worstFromTimedOverlap(appAudio, startMs, endMs, winStartMs);
}
export function joinCallQuality({ cdrItems, appAudio, window, filter = null }) {
const calls = groupCdrIntoCalls(cdrItems);
const joined = calls.map((call) => {
const wan = shouldAttachWan(call)
? worstPrismaBucketForCall(call, appAudio, window)
: null;
return { ...call, wan };
});
const scoped = filter ? filterCallsByTarget(joined, filter) : joined;
if (filter) {
return rebucketCalls(scoped);
}
return {
calls: joined,
legs: joined,
buckets: groupCallsByBucket(joined),
abnormalCalls: filterAbnormalCalls(joined),
summary: summarizeCalls(joined),
};
}
export function filterCdrToWindow(items, window) {
return (items || []).filter((item) => {
const start = cdrStartTime(item);
if (!start) return true;
return isWithinWindow(start, window);
});
}