// services/voicePin/resetVoicePortalPin.js // GET passcodeRules + PUT /v1/telephony/config/locations/{id}/voicePortal passcode import { logger } from '../../utils/logger.js'; import { generatePin } from './generatePin.js'; const LOG_SCOPE = 'resetvppin:service'; const MAX_PUT_ATTEMPTS = 5; /** * Reset a location's voice portal passcode. * * @param {string} locationId * @param {object} [opts] * @param {object} [opts.passcodeRules] cached rules from resolve step * @param {object} [opts.voicePortal] cached portal snapshot from resolve step * @param {object} [opts.webexClient] injectable client for tests * @returns {Promise} new PIN (never log this value) */ export async function resetVoicePortalPin(locationId, opts = {}) { if (!locationId) { throw new Error('locationId is required'); } const client = opts.webexClient || (await import('../../integrations/webex/WebexClient.js')).default; const passcodeRules = opts.passcodeRules || await client.request('GET', `telephony/config/locations/${locationId}/voicePortal/passcodeRules`); let lastError = null; for (let attempt = 0; attempt < MAX_PUT_ATTEMPTS; attempt += 1) { const passcode = generatePin({ rules: passcodeRules, extension: opts.voicePortal?.extension, }); const body = { passcode: { newPasscode: passcode, confirmPasscode: passcode, }, }; try { await client.request('PUT', `telephony/config/locations/${locationId}/voicePortal`, body); logger(LOG_SCOPE, `Voice portal PIN reset succeeded for location ${locationId}`, 'info'); return passcode; } catch (err) { lastError = err; const status = err?.response?.status; if (status === 400 && attempt < MAX_PUT_ATTEMPTS - 1) { logger(LOG_SCOPE, `Voice portal PIN rejected for ${locationId} (attempt ${attempt + 1}), retrying`, 'warn'); continue; } if (status === 400 || status === 422) { const existing = opts.voicePortal?.raw || await client.request('GET', `telephony/config/locations/${locationId}/voicePortal`).catch(() => null); if (existing) { const merged = { ...existing, passcode: body.passcode, }; try { await client.request('PUT', `telephony/config/locations/${locationId}/voicePortal`, merged); logger(LOG_SCOPE, `Voice portal PIN reset succeeded (merged PUT) for location ${locationId}`, 'info'); return passcode; } catch (mergeErr) { lastError = mergeErr; } } } throw lastError; } } throw lastError || new Error('Voice portal PIN reset failed'); }