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

164 lines
5.2 KiB
JavaScript

// services/callReport/callReportService.js
// Compose CDR + Media Quality + Prisma WAN for /callreport.
import { logger } from '../../utils/logger.js';
import { getHistoricalCallActivity } from '../phoneService.js';
import { findSdwanSiteForStore } from '../../integrations/paloalto/sites.js';
import { getAppMetric } from '../../integrations/paloalto/metrics.js';
import { buildAppDetailsUrl } from '../../integrations/paloalto/urls.js';
import {
resolveVoiceAppConfig,
summarizeAppSeries,
} from '../enrichment/sdwanEnrichment.js';
import { computeBusinessWindow } from './businessWindow.js';
import { resolveCallReportTarget } from './resolveTarget.js';
import { filterCdrToWindow, joinCallQuality } from './joinCallQuality.js';
import { cdrStartTime } from '../cdrFeedParser.js';
import { callReportEnvFlag } from './env.js';
const LOG_SCOPE = 'callreport:service';
const MQ_DISABLED = !callReportEnvFlag('ENABLE_WEBEX_MQ');
async function fetchCdrForWindow(target, window, { onCdrQueued } = {}) {
const cdr = await getHistoricalCallActivity(target.personId, 12, {
locationName: target.locationName,
startTime: window.startTime,
endTime: window.endTime,
returnRawItems: true,
skipPersonFilter: true,
onQueued: onCdrQueued,
});
if (!cdr.available) {
logger(
LOG_SCOPE,
`CDR unavailable for target=${target.label} loc=${JSON.stringify(target.locationName)}: ${cdr.reason}`,
'warn',
);
return { available: false, reason: cdr.reason, items: [], fetchErrors: cdr.fetchErrors };
}
const beforeFilter = cdr.rawItems?.length || 0;
const items = filterCdrToWindow(cdr.rawItems || [], window);
const rawCount = cdr.rawCount ?? beforeFilter;
if (rawCount > 0 && items.length === 0) {
const sample = (cdr.rawItems || []).slice(0, 2).map((r) => cdrStartTime(r) || 'no-start-field');
logger(
LOG_SCOPE,
`CDR window filter dropped all ${rawCount} record(s) for target=${target.label} ` +
`(window ${window.startTime}..${window.endTime}); sample start fields: ${sample.join(', ')}`,
'warn',
);
} else {
logger(
LOG_SCOPE,
`CDR target=${target.label} loc=${JSON.stringify(target.locationName)} ` +
`api=${rawCount} afterWindowFilter=${items.length}`,
'info',
);
}
return {
available: true,
items,
rawCount,
afterFilterCount: items.length,
};
}
async function fetchPrismaAppAudio(storeNum, window) {
const voiceCfg = resolveVoiceAppConfig();
if (!voiceCfg.appId) {
return { available: false, reason: 'PRISMA_APP_ID_VOICE not configured' };
}
if (!storeNum) {
return { available: false, reason: 'store number not resolved from location' };
}
const site = await findSdwanSiteForStore(storeNum);
if (!site?.id) {
return { available: false, reason: 'not a Prisma-managed store' };
}
const opts = { startTime: window.startTime, endTime: window.endTime };
const [mosRes, lossRes, jitterRes] = await Promise.all([
getAppMetric(site.id, voiceCfg.appId, 'mos', opts),
getAppMetric(site.id, voiceCfg.appId, 'loss', opts),
getAppMetric(site.id, voiceCfg.appId, 'jitter', opts),
]);
const appAudio = {
appId: voiceCfg.appId,
appName: voiceCfg.appName,
detailsUrl: buildAppDetailsUrl(site.id, voiceCfg.appId),
mos: summarizeAppSeries(mosRes, 'AppAudioMos'),
loss: summarizeAppSeries(lossRes, 'AppPerfUDPAudioPacketLoss'),
jitter: summarizeAppSeries(jitterRes, 'AppPerfUDPAudioJitter'),
};
return { available: true, site, appAudio };
}
/**
* @param {string} targetToken store number, email, or phone number
* @param {object} opts
* @param {string} [opts.dateArg] yesterday | today | YYYY-MM-DD
* @param {Date} [opts.now]
*/
export async function collectCallReport(targetToken, opts = {}) {
const startedAt = Date.now();
const target = await resolveCallReportTarget(targetToken);
const window = computeBusinessWindow({
timeZone: target.timeZone,
dateArg: opts.dateArg,
now: opts.now,
});
if (!window.ready) {
return {
ok: false,
target,
store: target,
window,
reason: window.reason,
};
}
logger(
LOG_SCOPE,
`Collecting call report target=${target.label} kind=${target.kind} loc=${target.locationName} ` +
`window=${window.startTime}..${window.endTime}`,
'info',
);
const [cdrRes, prismaRes] = await Promise.allSettled([
fetchCdrForWindow(target, window, { onCdrQueued: opts.onCdrQueued }),
fetchPrismaAppAudio(target.storeNum, window),
]);
const cdr = cdrRes.status === 'fulfilled'
? cdrRes.value
: { available: false, reason: cdrRes.reason?.message, items: [] };
const mediaQuality = { available: false, reason: MQ_DISABLED ? 'disabled' : 'not fetched', rows: [] };
const prisma = prismaRes.status === 'fulfilled'
? prismaRes.value
: { available: false, reason: prismaRes.reason?.message };
const joined = joinCallQuality({
cdrItems: cdr.items || [],
appAudio: prisma.appAudio || null,
window,
filter: target.filter || null,
});
return {
ok: true,
target,
store: target,
window,
cdr,
mediaQuality,
prisma,
joined,
fetchedAt: new Date().toISOString(),
elapsedMs: Date.now() - startedAt,
};
}