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

207 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// services/callReport/resolveTarget.js
// Parse and resolve /callreport targets: store, user email, or phone number.
import webex from '../../integrations/webex/WebexClient.js';
import { logger } from '../../utils/logger.js';
import { DISPLAY_TIMEZONE } from '../../utils/time.js';
import {
resolveNumberAssignment,
resolvePersonLocation,
} from '../callTest/locationResolver.js';
import { buildScopedCallFilter } from './filterCallsByTarget.js';
import {
getDectNetworksForPerson,
getPersonDetails,
getPersonIdByEmail,
} from '../phoneService.js';
import { resolveStoreForCallReport } from './storeContext.js';
import { parseCallReportTarget, parseStoreNumFromLocationName } from './parseTarget.js';
const LOG_SCOPE = 'callreport:target';
async function resolveLocationDetails(locationId) {
if (!locationId) return null;
return webex.request('GET', `telephony/config/locations/${locationId}`).catch(() => null);
}
async function resolveTimezone({ personId, locationId, locationDetails }) {
let timeZone = DISPLAY_TIMEZONE;
try {
const profile = personId
? await webex.request('GET', `telephony/config/people/${personId}`).catch(() => null)
: null;
const location = locationDetails || (locationId
? await resolveLocationDetails(locationId)
: null);
timeZone = profile?.timeZone || location?.timeZone || location?.timezone || timeZone;
} catch (err) {
logger(LOG_SCOPE, `Timezone lookup failed: ${err.message}`, 'debug');
}
return timeZone;
}
function buildStoreContextFromLocation({
kind,
label,
storeNum,
personId,
locationId,
locationName,
dialNumber,
timeZone,
email,
filter,
}) {
return {
kind,
label,
storeNum,
personId: personId || null,
locationId,
locationName,
dialNumber,
timeZone,
email: email || null,
filter: filter || null,
};
}
async function resolveStoreTarget(parsed) {
const store = await resolveStoreForCallReport(parsed.storeNum);
return buildStoreContextFromLocation({
kind: 'store',
label: `Store ${store.storeNum}`,
storeNum: store.storeNum,
personId: store.personId,
locationId: store.locationId,
locationName: store.locationName,
dialNumber: store.dialNumber,
timeZone: store.timeZone,
email: store.email,
filter: null,
});
}
async function resolveNumberTarget(parsed) {
const assignment = await resolveNumberAssignment(parsed.phoneNumber);
if (!assignment?.locationId) {
throw new Error(`No Webex location found for number ${parsed.phoneNumber}`);
}
const locationDetails = await resolveLocationDetails(assignment.locationId);
const dialNumber = locationDetails?.callingLineId?.phoneNumber
|| locationDetails?.phoneNumber
|| null;
const storeNum = parseStoreNumFromLocationName(assignment.locationName);
const timeZone = await resolveTimezone({
personId: assignment.owner?.id,
locationId: assignment.locationId,
locationDetails,
});
const filter = buildScopedCallFilter({
kind: 'number',
phoneNumber: parsed.phoneNumber,
personId: assignment.owner?.id || null,
extension: assignment.extension,
locationMain: dialNumber,
});
return buildStoreContextFromLocation({
kind: 'number',
label: parsed.phoneNumber,
storeNum,
personId: assignment.owner?.id || null,
locationId: assignment.locationId,
locationName: assignment.locationName,
dialNumber,
timeZone,
email: null,
filter,
});
}
async function resolveUserTarget(parsed) {
const personId = await getPersonIdByEmail(parsed.email);
if (!personId) {
throw new Error(`No Webex person found for ${parsed.email}`);
}
const person = await getPersonDetails(personId);
let locationId = null;
let locationName = null;
let ownedNumbers = [];
const personLoc = await resolvePersonLocation(personId);
if (personLoc) {
locationId = personLoc.locationId;
locationName = personLoc.locationName;
ownedNumbers = personLoc.ownedNumbers;
} else {
const dectNets = await getDectNetworksForPerson(personId);
if (dectNets.length > 0) {
locationId = dectNets[0].locationId && dectNets[0].locationId !== '—'
? dectNets[0].locationId
: null;
locationName = dectNets[0].locationName && dectNets[0].locationName !== '—'
? dectNets[0].locationName
: null;
}
}
if (!locationId || !locationName) {
throw new Error(`No Webex Calling location found for ${parsed.email}`);
}
const locationDetails = await resolveLocationDetails(locationId);
const dialNumber = locationDetails?.callingLineId?.phoneNumber
|| locationDetails?.phoneNumber
|| null;
const storeNum = parseStoreNumFromLocationName(locationName);
const timeZone = await resolveTimezone({ personId, locationId, locationDetails });
const filter = buildScopedCallFilter({
kind: 'user',
email: parsed.email,
personId,
person,
ownedNumbers,
locationMain: dialNumber,
});
if (ownedNumbers.length > 1) {
logger(
LOG_SCOPE,
`User ${parsed.email} has numbers at multiple locations; using ${locationName}`,
'warn',
);
}
return buildStoreContextFromLocation({
kind: 'user',
label: parsed.email,
storeNum,
personId,
locationId,
locationName,
dialNumber,
timeZone,
email: parsed.email,
filter,
});
}
/**
* @param {string} token raw first argument
* @returns {Promise<object>} unified call report target context
*/
export async function resolveCallReportTarget(token) {
const parsed = parseCallReportTarget(token);
if (!parsed) {
throw new Error('Invalid target — use a store number (24 digits), email, or phone number');
}
if (parsed.kind === 'store') return resolveStoreTarget(parsed);
if (parsed.kind === 'number') return resolveNumberTarget(parsed);
return resolveUserTarget(parsed);
}