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>
164 lines
5.2 KiB
JavaScript
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,
|
|
};
|
|
}
|