// src/services/voiceDiag/checks/dnd.js // // Detects whether the store's canonical Webex Calling user has Do // Not Disturb enabled. DND-on is a top-5 cause of "the store phone // doesn't ring" tickets — with DND on, incoming Webex calls skip the // device entirely and go straight to voicemail (or the configured // forward-when-no-answer target). // // Endpoint: GET/PUT /v1/people/{personId}/features/doNotDisturb // Payload: { enabled: boolean, ringSplashEnabled: boolean } // // Scope: spark-admin:people_read (GET) + spark-admin:people_write (PUT). // These are the scopes /webexhost / /offboarduser already use, // so no re-bootstrapping is needed. NOTE: the plan initially // referenced /v1/telephony/config/people/{id}/callSettings/* // but every one of those paths returns 404 "no static resource" // from the Webex API gateway — the current admin surface for // per-person call settings is `/v1/people/{id}/features/*`. // See wxc_sdk's user-call-settings table for the full list. // // Remediation: PUT `{enabled: false, ringSplashEnabled: false}`. We // force ringSplashEnabled off too on the reasonable assumption that a // store phone user in a diagnostic sweep doesn't want the visual // "someone is calling you" splash left dangling half-configured. If // this ever becomes a policy issue, split it into two remediations // and drop the ringSplash toggle here. import { logger } from '../../../utils/logger.js'; import { describeRequester } from '../../../utils/requester.js'; const ENDPOINT = (personId) => `people/${personId}/features/doNotDisturb`; // The desired-state contract for a store phone user. Exposed as a // top-level field on the check descriptor so the "voice standards" // reference table + regression tests can inspect it without reading // run() logic. Change the values here, not in the run() body. export const DND_STANDARDS = Object.freeze({ enabled: false, ringSplashEnabled: false, }); export const dndCheck = { id: 'dnd', label: 'Do Not Disturb', requires: ['personId'], scope: 'spark-admin:people_read', standards: DND_STANDARDS, async run(ctx) { const data = await ctx.webex.request('GET', ENDPOINT(ctx.personId)); const enabled = !!data?.enabled; const ringSplashEnabled = !!data?.ringSplashEnabled; // Compliance = DND off. ringSplashEnabled only matters when DND // is on (per Webex spec — it's the "visual reminder" toggle for // splash notifications while calls are being silenced), so we // don't count a stale ringSplash=true as a deviation while // DND is already off. The standard's ringSplashEnabled=false is // the value we PUT during remediation, not an independent rule. if (enabled === DND_STANDARDS.enabled) { return { status: 'ok', message: 'DND is off.', details: { enabled, ringSplashEnabled }, remediation: null, }; } return { status: 'warn', message: 'DND is enabled — incoming calls will be silenced on this user\'s phones.', details: { enabled, ringSplashEnabled }, remediation: { action: 'disable_dnd', title: 'Disable DND', summary: `Turn DND off for ${ctx.personLabel}.`, payload: { personId: ctx.personId, personLabel: ctx.personLabel, storeNum: ctx.storeNum, // Snapshot of the "before" state so the audit log line // reads correctly after we PUT the new value. before: { enabled, ringSplashEnabled }, }, }, }; }, remediations: { async disable_dnd(bot, data, requester) { const { personId, personLabel, storeNum, before } = data; logger( 'voicediag:audit', `CONFIRMED disable_dnd for ${personLabel} (person=${personId}, store=${storeNum}) ` + `by ${describeRequester(requester)} — was enabled=${before?.enabled ?? 'unknown'}, ` + `ringSplashEnabled=${before?.ringSplashEnabled ?? 'unknown'}`, ); try { // Import lazily so unit tests can replace the exported webex // singleton via ctx.webex in run(), while the remediation path // stays honest about which client it uses in production. const { default: webex } = await import('../../../integrations/webex/WebexClient.js'); await webex.request('PUT', ENDPOINT(personId), { ...DND_STANDARDS }); } catch (err) { logger( 'voicediag:audit', `FAILED disable_dnd for ${personLabel}: ${err.message}`, 'error', ); await bot.say( 'markdown', `❌ Failed to disable DND for **${personLabel}**: ${err.message}`, ); return; } await bot.say( 'markdown', `✅ DND disabled for **${personLabel}** (store ${storeNum}). ` + `Re-run \`/voicediag ${storeNum}\` to verify.`, ); logger( 'voicediag:audit', `COMPLETED disable_dnd for ${personLabel} (store ${storeNum})`, ); }, }, };