Refactor every /voicediag check to declare a top-level `standards` object so the desired state is legible without reading run() logic and can drive a documented reference table. Upgrade callForwarding to error severity, tighten voicemail with three send-to-VM error paths + a `stop_sending_to_voicemail` remediation, and add a `disable_hoteling` remediation. Add a port-hygiene check bucket under services/voiceDiag/checks/port (portType, portVlan, portPoe, portEnabled) that reuses the phone- status snapshot to enforce switchport standards. Configurable via VOICE_STANDARD_PHONE_VLAN (default 102) and VOICE_STANDARD_ENABLED (kill-switch). Preserve Meraki `portType`/`voiceVlan`/`dataVlan` through the enrichment chain so the checks have clean data to read. Add an "apply all N fixes" combined card that shows up when 2+ remediations are available. New confirm_voicediag_all / cancel_voicediag_all actions run each fix in sequence (readable audit trail, no per-person write-throttle stacking), accumulate individual failures into a summary rather than aborting. Adds regression tests asserting every check exposes .standards, plus coverage for port checks, kill-switch, and combined-card iteration. 63 tests in the checks file, 188 total, all green. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.9 KiB
JavaScript
108 lines
3.9 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',
|
|
]);
|
|
|
|
// Store-line standard: no HIGH_IMPACT_CALL_TYPE may be BLOCKED. Any
|
|
// other rule (BLOCK on premium/casual/operator, ALLOW on everything)
|
|
// is fine. Documented as data so the standards reference table +
|
|
// tests can enumerate what "compliant" means.
|
|
export const OUTGOING_PERMISSION_STANDARDS = Object.freeze({
|
|
highImpactCallTypes: Array.from(HIGH_IMPACT_CALL_TYPES),
|
|
highImpactBlockedAllowed: false,
|
|
});
|
|
|
|
export const outgoingPermissionCheck = {
|
|
id: 'outgoingPermission',
|
|
label: 'Outgoing Call Permissions',
|
|
requires: ['personId'],
|
|
scope: 'spark-admin:people_read',
|
|
standards: OUTGOING_PERMISSION_STANDARDS,
|
|
|
|
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,
|
|
};
|
|
},
|
|
};
|