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>
186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
// commands/resetVpPin.js
|
||
//
|
||
// /resetvppin <store> — reset a location's voice portal PIN with confirmation card.
|
||
//
|
||
// Store number only (2–4 digits). Used to manage auto-attendant greetings.
|
||
|
||
import { logger } from '../utils/logger.js';
|
||
import { pendingVpPinResets } from '../utils/pendingVpPinResets.js';
|
||
import { extractRequester, describeRequester } from '../utils/requester.js';
|
||
import {
|
||
parseVpPinStore,
|
||
resolveVpPinStore,
|
||
} from '../services/voicePin/resolveVpPinStore.js';
|
||
import { resetVoicePortalPin } from '../services/voicePin/resetVoicePortalPin.js';
|
||
|
||
function buildConfirmationCard(cardId, resolved) {
|
||
const portal = resolved.voicePortal || {};
|
||
const dialIn = portal.phoneNumber
|
||
? `${portal.phoneNumber}${portal.extension ? ` ext ${portal.extension}` : ''}`
|
||
: '—';
|
||
|
||
return {
|
||
type: 'AdaptiveCard',
|
||
version: '1.3',
|
||
body: [
|
||
{
|
||
type: 'TextBlock',
|
||
text: '🔐 RESET VOICE PORTAL PIN',
|
||
weight: 'Bolder',
|
||
size: 'Large',
|
||
color: 'Attention',
|
||
},
|
||
{
|
||
type: 'TextBlock',
|
||
text:
|
||
'Confirming will generate a new voice portal passcode for this location. ' +
|
||
'This PIN is used to dial in and manage **auto-attendant greetings**.',
|
||
wrap: true,
|
||
spacing: 'Small',
|
||
},
|
||
{
|
||
type: 'FactSet',
|
||
spacing: 'Medium',
|
||
facts: [
|
||
{ title: 'Store', value: resolved.storeNum },
|
||
{ title: 'Location', value: resolved.locationName || '—' },
|
||
{ title: 'Location ID', value: resolved.locationId },
|
||
{ title: 'Voice portal', value: portal.name || '—' },
|
||
{ title: 'Dial-in', value: dialIn },
|
||
{ title: 'Main number', value: resolved.mainNumber || '—' },
|
||
],
|
||
},
|
||
],
|
||
actions: [
|
||
{
|
||
type: 'Action.Submit',
|
||
title: '✅ Confirm Reset',
|
||
data: { action: 'confirm_reset_vppin', cardId },
|
||
},
|
||
{
|
||
type: 'Action.Submit',
|
||
title: '❌ Cancel',
|
||
data: { action: 'cancel_reset_vppin', cardId },
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
export async function applyResetVpPinConfirmation(bot, data, _roomId, requester) {
|
||
logger(
|
||
'resetvppin:audit',
|
||
`CONFIRMED voice portal PIN reset for store ${data.storeNum} (location=${data.locationId}) ` +
|
||
`by ${describeRequester(requester)}`,
|
||
);
|
||
|
||
try {
|
||
const newPin = await resetVoicePortalPin(data.locationId, {
|
||
passcodeRules: data.passcodeRules,
|
||
voicePortal: data.voicePortal,
|
||
});
|
||
const portal = data.voicePortal || {};
|
||
const dialIn = portal.phoneNumber
|
||
? `${portal.phoneNumber}${portal.extension ? ` ext ${portal.extension}` : ''}`
|
||
: null;
|
||
|
||
await bot.say(
|
||
'markdown',
|
||
`✅ Voice portal PIN reset for **Store ${data.storeNum}** ` +
|
||
`(\`${data.locationName || data.locationId}\`).\n\n` +
|
||
`**New voice portal PIN:** \`${newPin}\`\n` +
|
||
(dialIn ? `\nDial in at **${dialIn}** to manage auto-attendant greetings.\n` : '\n') +
|
||
'_Share this PIN only in trusted spaces._',
|
||
);
|
||
logger(
|
||
'resetvppin:audit',
|
||
`COMPLETED voice portal PIN reset for store ${data.storeNum} (location=${data.locationId}) ` +
|
||
`by ${describeRequester(requester)}`,
|
||
);
|
||
} catch (err) {
|
||
const apiMsg = err?.response?.data?.message || err.message;
|
||
logger(
|
||
'resetvppin:audit',
|
||
`FAILED voice portal PIN reset for store ${data.storeNum}: ${apiMsg}`,
|
||
'error',
|
||
);
|
||
await bot.say(
|
||
'markdown',
|
||
`❌ Voice portal PIN reset failed for **Store ${data.storeNum}**: ${apiMsg}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function cancelResetVpPinCard(bot, data, _roomId, requester) {
|
||
await bot.say(
|
||
'markdown',
|
||
`❌ Voice portal PIN reset cancelled for **Store ${data.storeNum}**. No changes were made.`,
|
||
);
|
||
logger(
|
||
'resetvppin:audit',
|
||
`CANCELLED voice portal PIN reset for store ${data.storeNum} by ${describeRequester(requester)}`,
|
||
);
|
||
}
|
||
|
||
export async function handleResetVpPin(bot, trigger) {
|
||
const args = trigger.args || [];
|
||
const query = trigger.query || {};
|
||
const raw = (args[0] || query.store || query.storeNum || query.s || '').trim();
|
||
|
||
if (!raw) {
|
||
await bot.say(
|
||
'markdown',
|
||
'**Usage:** `/resetvppin <store>`\n\n' +
|
||
'Provide a 2–4 digit store number (e.g. `782`).',
|
||
);
|
||
return;
|
||
}
|
||
|
||
const parsed = parseVpPinStore(raw);
|
||
if (!parsed) {
|
||
await bot.say(
|
||
'markdown',
|
||
`❌ Invalid store \`${raw}\`. Use a 2–4 digit store number.`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
const requester = extractRequester(trigger);
|
||
logger(
|
||
'resetvppin:audit',
|
||
`REQUESTED voice portal PIN reset card for store ${parsed.storeNum} by ${describeRequester(requester)}`,
|
||
);
|
||
|
||
try {
|
||
const resolved = await resolveVpPinStore(parsed);
|
||
|
||
if (!trigger.person) {
|
||
await bot.say(
|
||
'markdown',
|
||
`Resolved **Store ${resolved.storeNum}** → \`${resolved.locationName || resolved.locationId}\`. ` +
|
||
'Confirmation cards are only available in Webex chat.',
|
||
);
|
||
return;
|
||
}
|
||
|
||
const cardId = `vppin-${Date.now()}`;
|
||
pendingVpPinResets.set(cardId, {
|
||
...resolved,
|
||
roomId: trigger.roomId || trigger.message?.roomId,
|
||
requester,
|
||
});
|
||
|
||
const adaptiveCard = buildConfirmationCard(cardId, resolved);
|
||
await bot.say({
|
||
markdown:
|
||
`Review voice portal PIN reset for **Store ${resolved.storeNum}** ` +
|
||
`(\`${resolved.locationName || resolved.locationId}\`):`,
|
||
attachments: [{
|
||
contentType: 'application/vnd.microsoft.card.adaptive',
|
||
content: adaptiveCard,
|
||
}],
|
||
});
|
||
} catch (err) {
|
||
logger('resetvppin', `Resolve failed for store ${raw}: ${err.message}`, 'warn');
|
||
await bot.say('markdown', `❌ ${err.message}`);
|
||
}
|
||
}
|