Introduces a new diagnostic command that walks a registry of check
modules against a store user's Webex Calling configuration and
surfaces per-issue adaptive-card remediation for the fixable ones.
Checks (services/voiceDiag/checks/): dnd, callForwarding, callWaiting,
callIntercept, voicemail, hoteling, executiveAssistant,
outgoingPermission, phoneOnline. Remediations offered for DND,
forwarding, waiting, and intercept.
Uses the /v1/people/{id}/features/* admin surface (spark-admin:people_read
+ spark-admin:people_write scopes we already hold) — the earlier
telephony/config/people/*/callSettings/* path scheme returns 404 from
the Webex gateway and is not a live surface. Runner distinguishes
routing-404s ("URL moved") from "not applicable" 404s ("no calling
license") via the response body.
Arg parser accepts detail/detailed/--detail/--detailed and normalises
macOS smart-dashes so --detailed doesn't die when auto-correct
turns it into an em-dash.
Wires a VOICEDIAG_ACTIONS dispatcher in index.js mirroring the IGMP
branch, and registers /voicediag in commands/registry.js. 170 tests
pass (52 new: 39 check + 12 renderer + 5 arg-normalization).
Docs updated in .env.example, services/phoneService.js:467, and a new
services/voiceDiag/README.md that includes a "how to add a check"
recipe plus a note on the earlier wrong URL scheme.
Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.4 KiB
JavaScript
98 lines
3.4 KiB
JavaScript
// src/services/voiceDiag/checks/outgoingPermission.js
|
|
//
|
|
// Info-only check for outgoing call permissions. Extends the
|
|
// count-only surface currently in `/phonestatus` with per-rule
|
|
// detail. When a call type is set to BLOCK (rather than ALLOW /
|
|
// AUTH_CODE / TRANSFER), users on that line can't dial calls of that
|
|
// class. Blocked domestic-toll or international are especially
|
|
// common tickets ("we can't dial out from the store"), so we flag
|
|
// those as a warning.
|
|
//
|
|
// Endpoint: GET /v1/people/{personId}/features/outgoingPermission
|
|
// Scope: spark-admin:people_read
|
|
// Response shape:
|
|
// {
|
|
// useCustomEnabled: boolean,
|
|
// callingPermissions: [
|
|
// { callType: 'INTERNAL_CALL' | 'LOCAL' | 'TOLL_FREE' | 'TOLL' |
|
|
// 'NATIONAL' | 'INTERNATIONAL' | 'OPERATOR_ASSISTED' |
|
|
// 'CHARGEABLE_DIRECTORY_ASSISTED' | 'SPECIAL_SERVICES_I'|II |
|
|
// 'PREMIUM_SERVICES_I'|II | 'CASUAL',
|
|
// action: 'ALLOW' | 'BLOCK' | 'AUTH_CODE' | 'TRANSFER_NUMBER_1'..3,
|
|
// transferEnabled: boolean }
|
|
// ]
|
|
// }
|
|
//
|
|
// When `useCustomEnabled=false` the account defers to the site's
|
|
// default outgoing permission set — in that case there's nothing
|
|
// user-scoped to report, so we return OK with a note.
|
|
|
|
const ENDPOINT = (personId) =>
|
|
`people/${personId}/features/outgoingPermission`;
|
|
|
|
// Call types that being BLOCKED tends to be a bug for a store line.
|
|
// Others (premium/casual/operator) are almost always intentionally
|
|
// blocked, so blocking them is fine.
|
|
const HIGH_IMPACT_CALL_TYPES = new Set([
|
|
'LOCAL',
|
|
'NATIONAL',
|
|
'TOLL_FREE',
|
|
'TOLL',
|
|
'INTERNATIONAL',
|
|
]);
|
|
|
|
export const outgoingPermissionCheck = {
|
|
id: 'outgoingPermission',
|
|
label: 'Outgoing Call Permissions',
|
|
requires: ['personId'],
|
|
scope: 'spark-admin:people_read',
|
|
|
|
async run(ctx) {
|
|
const data = await ctx.webex.request('GET', ENDPOINT(ctx.personId));
|
|
|
|
const useCustomEnabled = !!data?.useCustomEnabled;
|
|
const rules = Array.isArray(data?.callingPermissions) ? data.callingPermissions : [];
|
|
|
|
if (!useCustomEnabled) {
|
|
return {
|
|
status: 'ok',
|
|
message: 'Using the location default outgoing permission set (no user override).',
|
|
details: { useCustomEnabled, rules },
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
const blocked = rules
|
|
.filter((r) => r?.action === 'BLOCK')
|
|
.map((r) => r.callType);
|
|
|
|
const highImpactBlocked = blocked.filter((t) => HIGH_IMPACT_CALL_TYPES.has(t));
|
|
|
|
if (highImpactBlocked.length > 0) {
|
|
return {
|
|
status: 'warn',
|
|
message:
|
|
`Custom outgoing permissions are set and the following high-impact call types ` +
|
|
`are BLOCKED: ${highImpactBlocked.join(', ')}. Users on this line will hear a fast-busy for those calls.`,
|
|
details: { useCustomEnabled, rules, blocked, highImpactBlocked },
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
if (blocked.length > 0) {
|
|
return {
|
|
status: 'ok',
|
|
message: `Custom outgoing permissions with ${blocked.length} blocked call type(s): ${blocked.join(', ')}. None are high-impact.`,
|
|
details: { useCustomEnabled, rules, blocked, highImpactBlocked: [] },
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'ok',
|
|
message: `Custom outgoing permissions (${rules.length} rules) — nothing is blocked.`,
|
|
details: { useCustomEnabled, rules, blocked: [], highImpactBlocked: [] },
|
|
remediation: null,
|
|
};
|
|
},
|
|
};
|