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>
207 lines
5.8 KiB
JavaScript
207 lines
5.8 KiB
JavaScript
// 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 (2–4 digits), email, or phone number');
|
||
}
|
||
|
||
if (parsed.kind === 'store') return resolveStoreTarget(parsed);
|
||
if (parsed.kind === 'number') return resolveNumberTarget(parsed);
|
||
return resolveUserTarget(parsed);
|
||
}
|