// 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, }; }