Chat-only commands reset voicemail mailbox and voice portal passcodes via telephony_config_write, with extension lookup and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.9 KiB
JavaScript
126 lines
3.9 KiB
JavaScript
// services/voicePin/resolveVmPinTarget.js
|
||
// Parse and resolve /resetvmpin targets: email, store (2–4 digits), or extension (5 digits).
|
||
|
||
import { logger } from '../../utils/logger.js';
|
||
import { getPersonDetails } from '../phoneService.js';
|
||
import {
|
||
resolvePersonLocation,
|
||
resolveRecordsByExtension,
|
||
} from '../callTest/locationResolver.js';
|
||
|
||
export { parseVmPinTarget } from './parseVmPinTarget.js';
|
||
|
||
const LOG_SCOPE = 'resetvmpin:resolve';
|
||
|
||
/**
|
||
* @typedef {'email'|'store'|'extension'} VmPinMatchSource
|
||
*/
|
||
|
||
async function getWebex() {
|
||
const mod = await import('../../integrations/webex/WebexClient.js');
|
||
return mod.default;
|
||
}
|
||
|
||
async function fetchVoicemailSnapshot(personId) {
|
||
if (!personId) return { enabled: null };
|
||
try {
|
||
const webex = await getWebex();
|
||
const vm = await webex.request('GET', `people/${personId}/features/voicemail`);
|
||
return { enabled: vm?.enabled === true, raw: vm };
|
||
} catch (err) {
|
||
logger(LOG_SCOPE, `Voicemail feature fetch failed for ${personId}: ${err.message}`, 'warn');
|
||
return { enabled: null };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resolve a parsed VM PIN target to a Webex person + context for the confirmation card.
|
||
*
|
||
* @param {ReturnType<typeof parseVmPinTarget>} parsed
|
||
* @returns {Promise<object>}
|
||
*/
|
||
export async function resolveVmPinTarget(parsed) {
|
||
if (!parsed) {
|
||
throw new Error('Invalid target. Use email, 2–4 digit store number, or 5-digit extension.');
|
||
}
|
||
|
||
const webex = await getWebex();
|
||
let personId = null;
|
||
let email = null;
|
||
let matchSource;
|
||
let storeNum = null;
|
||
let extension = null;
|
||
let numberRecord = null;
|
||
|
||
if (parsed.kind === 'email') {
|
||
matchSource = 'email';
|
||
email = parsed.email;
|
||
const person = await webex.findPersonByEmail(email);
|
||
if (!person) {
|
||
throw new Error(`No Webex person found for ${email}`);
|
||
}
|
||
personId = person.id;
|
||
} else if (parsed.kind === 'store') {
|
||
matchSource = 'store';
|
||
storeNum = parsed.storeNum;
|
||
const padded = storeNum.padStart(5, '0');
|
||
email = `ae${padded}@ae.com`;
|
||
const person = await webex.findPersonByEmail(email);
|
||
if (!person) {
|
||
throw new Error(`No Webex person found for store ${storeNum} (${email})`);
|
||
}
|
||
personId = person.id;
|
||
} else {
|
||
matchSource = 'extension';
|
||
extension = parsed.extension;
|
||
const records = await resolveRecordsByExtension(extension);
|
||
if (records.length === 0) {
|
||
throw new Error(`No telephony number found for extension ${extension}`);
|
||
}
|
||
const ownerIds = [...new Set(records.map((r) => r.owner?.id).filter(Boolean))];
|
||
if (ownerIds.length !== 1) {
|
||
throw new Error(
|
||
`Extension ${extension} matches ${records.length} number(s) across ${ownerIds.length} owner(s) — cannot resolve a unique user`,
|
||
);
|
||
}
|
||
personId = ownerIds[0];
|
||
numberRecord = records[0];
|
||
}
|
||
|
||
const [person, locationInfo, voicemail] = await Promise.all([
|
||
getPersonDetails(personId),
|
||
resolvePersonLocation(personId),
|
||
fetchVoicemailSnapshot(personId),
|
||
]);
|
||
|
||
if (!person) {
|
||
throw new Error(`Could not load Webex person ${personId}`);
|
||
}
|
||
|
||
if (!email) {
|
||
email = (person.emails || []).find((e) => typeof e === 'string' && e.includes('@'))
|
||
|| person.emails?.[0]?.value
|
||
|| person.emails?.[0]
|
||
|| null;
|
||
}
|
||
|
||
const ownedNumbers = locationInfo?.ownedNumbers || [];
|
||
const primaryNumber = numberRecord
|
||
|| ownedNumbers.find((n) => n.extension === extension)
|
||
|| ownedNumbers[0]
|
||
|| null;
|
||
|
||
return {
|
||
personId,
|
||
person,
|
||
email: email || person.displayName || personId,
|
||
displayName: person.displayName || email || personId,
|
||
matchSource,
|
||
storeNum,
|
||
extension: extension || primaryNumber?.extension || null,
|
||
locationId: locationInfo?.locationId || primaryNumber?.locationId || null,
|
||
locationName: locationInfo?.locationName || primaryNumber?.locationName || null,
|
||
mainNumber: primaryNumber?.phoneNumber || null,
|
||
voicemailEnabled: voicemail.enabled,
|
||
};
|
||
}
|