// src/services/voiceDiag/checks/voicemail.js // // Detects voicemail configuration for the store user. Store-line // standard is: // // - voicemail ENABLED (so the user can access the box directly if // they ever need to), but // - NO call should ever be routed to voicemail — the three // "send to VM" triggers (sendAllCalls / sendBusyCalls / // sendUnansweredCalls) must all be OFF. // - MWI (message-waiting indicator) on so any manually-left // messages light up the phone. // // This is stricter than the initial cut. The rationale: at AE, a // store phone that goes to VM is a lost customer call. Better for // the phone to keep ringing (or drop) than to silently swallow the // call into a VM box nobody checks. // // Endpoint: GET/PUT /v1/people/{personId}/features/voicemail // Scope: spark-admin:people_read (GET) + spark-admin:people_write (PUT). // Response shape (relevant fields): // { // enabled: true, // sendAllCalls: { enabled: false }, // sendBusyCalls: { enabled: false, greeting: 'DEFAULT' }, // sendUnansweredCalls: { enabled: true, ... }, // notifications: { enabled: false, destination: '' }, // transferToNumber: { enabled: false, destination: '' }, // emailCopyOfMessage: { enabled: false, emailId: '' }, // messageStorage: { // mwiEnabled: true, // storageType: 'INTERNAL' | 'EXTERNAL', // externalEmail: '' // }, // faxMessage: { ... } // } // // PUT semantics: partial updates work — you can send just the // send-*Calls sub-blocks and the other fields are preserved. // Remediation takes advantage of that to make a minimal-touch PUT. import { logger } from '../../../utils/logger.js'; import { describeRequester } from '../../../utils/requester.js'; const ENDPOINT = (personId) => `people/${personId}/features/voicemail`; const AE_EMAIL_RE = /@ae\.com$/i; export const VOICEMAIL_STANDARDS = Object.freeze({ enabled: true, sendAllCalls: { enabled: false }, sendBusyCalls: { enabled: false }, sendUnansweredCalls: { enabled: false }, messageStorage: { mwiEnabled: true }, }); export const voicemailCheck = { id: 'voicemail', label: 'Voicemail', requires: ['personId'], scope: 'spark-admin:people_read', standards: VOICEMAIL_STANDARDS, async run(ctx) { const data = await ctx.webex.request('GET', ENDPOINT(ctx.personId)); const enabled = !!data?.enabled; const storage = data?.messageStorage || {}; const mwiEnabled = !!storage?.mwiEnabled; const emailCopy = data?.emailCopyOfMessage || {}; const transferTo = data?.transferToNumber || {}; const externalEmail = storage?.externalEmail || ''; const forwardEmailTarget = emailCopy?.enabled ? (emailCopy?.emailId || '') : ''; const sendAllCallsEnabled = !!data?.sendAllCalls?.enabled; const sendBusyCallsEnabled = !!data?.sendBusyCalls?.enabled; const sendUnansweredCallsEnabled = !!data?.sendUnansweredCalls?.enabled; const activeSendTriggers = []; if (sendAllCallsEnabled) activeSendTriggers.push('sendAllCalls'); if (sendBusyCallsEnabled) activeSendTriggers.push('sendBusyCalls'); if (sendUnansweredCallsEnabled) activeSendTriggers.push('sendUnansweredCalls'); const details = { enabled, mwiEnabled, storageType: storage?.storageType || null, externalEmail, emailCopyEnabled: !!emailCopy?.enabled, emailCopyTarget: forwardEmailTarget, transferToEnabled: !!transferTo?.enabled, transferToDestination: transferTo?.destination || null, sendAllCallsEnabled, sendBusyCallsEnabled, sendUnansweredCallsEnabled, activeSendTriggers, }; // Hard rule #1: VM enabled at all. If VM is disabled we surface // it as a warn rather than an error — some sites intentionally // disable VM and route unanswered calls elsewhere, so we don't // auto-remediate. if (!enabled) { return { status: 'warn', message: 'Voicemail is disabled — callers will not be able to leave messages.', details, remediation: null, }; } // Hard rule #2: none of the three send-to-VM triggers may be // active. This is the store-line standard: VM exists so the // operator can check the box manually, but no call ever gets // silently swallowed into it. if (activeSendTriggers.length > 0) { const humanTriggers = activeSendTriggers .map((k) => k.replace(/([A-Z])/g, ' $1').toLowerCase().trim()) .join(', '); return { status: 'error', message: `Voicemail is receiving calls: ${humanTriggers} ${activeSendTriggers.length === 1 ? 'trigger is' : 'triggers are'} active. ` + `Store phone standard requires voicemail be reachable manually but never used as a call target.`, details, remediation: { action: 'stop_sending_to_voicemail', title: 'Stop sending calls to voicemail', summary: `Disable ${activeSendTriggers.length} VM trigger(s) for ${ctx.personLabel} so incoming calls stop being routed to voicemail. ` + `Voicemail itself stays enabled.`, payload: { personId: ctx.personId, personLabel: ctx.personLabel, storeNum: ctx.storeNum, activeSendTriggers, before: { sendAllCallsEnabled, sendBusyCallsEnabled, sendUnansweredCallsEnabled, }, }, }, }; } // Soft rules below — no auto-remediation, just visibility. if (!mwiEnabled) { return { status: 'warn', message: 'Voicemail is enabled but MWI (message-waiting indicator) is off — new messages won\'t light up the phone.', details, remediation: null, }; } if (forwardEmailTarget && !AE_EMAIL_RE.test(forwardEmailTarget)) { return { status: 'warn', message: `Voicemail-to-email is forwarding to an off-org address: ${forwardEmailTarget}. Verify this is intentional.`, details, remediation: null, }; } if (details.transferToEnabled && !details.transferToDestination) { return { status: 'warn', message: 'Voicemail transfer-to-number is enabled but no destination is set — will fall through to the default greeting.', details, remediation: null, }; } return { status: 'ok', message: 'Voicemail is enabled, no send-to-VM triggers active, MWI on. Compliant.', details, remediation: null, }; }, remediations: { async stop_sending_to_voicemail(bot, data, requester) { const { personId, personLabel, storeNum, activeSendTriggers, before } = data; if (!Array.isArray(activeSendTriggers) || activeSendTriggers.length === 0) { await bot.say( 'markdown', `⚠️ No send-to-VM triggers to clear for **${personLabel}** — nothing to do.`, ); return; } logger( 'voicediag:audit', `CONFIRMED stop_sending_to_voicemail for ${personLabel} (person=${personId}, store=${storeNum}) ` + `by ${describeRequester(requester)} — clearing=${activeSendTriggers.join(',')}, ` + `before=${JSON.stringify(before)}`, ); try { const { default: webex } = await import('../../../integrations/webex/WebexClient.js'); // Minimal PUT: only the three send-* sub-blocks. Voicemail // itself stays enabled + all other config (greetings, // storage, notifications) is preserved by Webex's partial- // update semantics on this endpoint. const body = { sendAllCalls: { ...VOICEMAIL_STANDARDS.sendAllCalls }, sendBusyCalls: { ...VOICEMAIL_STANDARDS.sendBusyCalls }, sendUnansweredCalls: { ...VOICEMAIL_STANDARDS.sendUnansweredCalls }, }; await webex.request('PUT', ENDPOINT(personId), body); } catch (err) { logger( 'voicediag:audit', `FAILED stop_sending_to_voicemail for ${personLabel}: ${err.message}`, 'error', ); await bot.say( 'markdown', `❌ Failed to stop send-to-VM for **${personLabel}**: ${err.message}`, ); return; } await bot.say( 'markdown', `✅ Voicemail send triggers cleared for **${personLabel}** (store ${storeNum}): ${activeSendTriggers.join(', ')}. ` + `Voicemail is still enabled but no calls will be routed to it. ` + `Re-run \`/voicediag ${storeNum}\` to verify.`, ); logger( 'voicediag:audit', `COMPLETED stop_sending_to_voicemail for ${personLabel} (store ${storeNum}) — cleared=${activeSendTriggers.join(',')}`, ); }, }, };