From b5610acc8efde4f4c6ea50abe15a3d265d777877 Mon Sep 17 00:00:00 2001 From: jmcqueen Date: Mon, 6 Jul 2026 16:33:51 -0400 Subject: [PATCH] Retire /buildStore, rename /migrateStore -> /finalizeStore, harden flows - Delete /buildStore command + flow (dead since phone numbers now have to be purchased before provisioning); drop the only cascading dead helper (updateLocationRouteGroup) that was only called by that flow - Rename /migrateStore -> /finalizeStore. "Migrate" was a leftover from the legacy-system era; the command finishes what stage started - Rename buildStoreInfoCard -> storeConfirmationCard (misleading name since all flows share it); rename btnBuildStore/btnBadInfo -> btnConfirm/btnCancel to match - Fix the double-write of storeInfo.extension: siw.js sets a 5XXXX default and both command handlers were overriding it (stage to 8XXXX, finalize back to 5XXXX). Drop both overrides; single source of truth - Add /finalizeStore preflight that runs findWebexLocation in parallel with findWebexUser; if the location doesn't exist, bail before showing the confirmation card with an actionable "Run /stageStore first" line - Add post-run next-step summaries: stage points at /finalizeStore , finalize confirms the location is live - Update README (commands, mermaid) and deviceCustomizations header Co-authored-by: Cursor --- README.md | 33 ++++++++------ src/cards/storeConfirmationCard.js | 11 +++-- src/commands/attachmentActions.js | 10 ++-- src/commands/buildStore.js | 30 ------------ src/commands/finalizeStore.js | 35 ++++++++++---- src/commands/stageStore.js | 7 ++- src/flows/buildStore.js | 73 ------------------------------ src/flows/finalizeStore.js | 15 ++++-- src/flows/stageStore.js | 7 ++- src/index.js | 6 +-- src/webex/deviceCustomizations.js | 2 +- src/webex/locations.js | 11 ----- 12 files changed, 75 insertions(+), 165 deletions(-) delete mode 100644 src/commands/buildStore.js delete mode 100644 src/flows/buildStore.js diff --git a/README.md b/README.md index 7eccdd5..8d6b509 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,10 @@ the flag Node throws `SELF_SIGNED_CERT_IN_CHAIN` even for Google's public certs. Trust is delegated to that proxy by policy, so the scoped bypass is consistent across all proxied traffic. -Once the agent is connected, the bot logs `Remote agent connected` and any -`/buildStore`, `/stageStore`, `/migrateStore` command will succeed. If the -agent is not connected, those commands fail immediately with -`No remote SIW agent connected` rather than silently timing out. +Once the agent is connected, the bot logs `Remote agent connected` and +`/stageStore` and `/finalizeStore` will succeed. If the agent is not +connected, those commands fail immediately with a preflight message +rather than silently timing out mid-flow. ### Deploying the agent @@ -127,17 +127,22 @@ for the full operator guide (upgrades, troubleshooting, coexistence with Registered in [src/commands](src/commands): -- `/buildStore ` — full green-field build (create location, calling, - greeting, attach user, license cleanup). -- `/stageStore ` — pre-migration setup: same as buildStore but - without phone-number attachment or licensing cleanup. -- `/migrateStore ` — cut-over for a staged store: attach the phone - number, set caller ID, create the auto-attendant, finalize licensing. +- `/stageStore ` — creates the Webex location, enables calling, + configures the schedule, greeting, music-on-hold, voicemail, voice portal, + and attaches the Webex Calling license to the store user. Everything + except the phone number, which has to be purchased separately in Control Hub. +- `/finalizeStore ` — cut-over for a previously staged store: + attaches the purchased phone number, sets caller ID, creates the + auto-attendant, and finalizes user licensing. Preflights that the store + was actually staged first. - `/storeinfo ` — show current Webex info for the store user (`ae<5-digit>@ae.com`). - `/userinfo ` — show current Webex info for any user by email. - `/help` — bot's own help output. +Typical provisioning workflow: `/stageStore 499` → purchase a phone number +in Control Hub → `/finalizeStore 499`. + Card confirmations post `attachmentAction` events, dispatched in [src/commands/attachmentActions.js](src/commands/attachmentActions.js). @@ -153,7 +158,7 @@ src/ webex/ all Webex API calls, one module per resource family integrations/ SIW / Twilio / Google cards/ Adaptive Card builders - flows/ multi-step provisioning (build, stage, migrate, ...) + flows/ multi-step provisioning (stage, finalize) commands/ framework.hears handlers + attachmentAction dispatch scripts/ one-off maintenance scripts (911 CSV, phone fix-up) greetings/ WAV files uploaded as location announcements @@ -164,12 +169,12 @@ Data flow for a store provisioning: ```mermaid flowchart LR - User[Webex user] -->|/buildStore 1234| Commands[commands/*] + User[Webex user] -->|/stageStore 1234| Commands[commands/*] Commands --> SIW[integrations/siw.js] Commands --> Users[webex/users.js] - Commands --> Card[cards/storeInfoCard.js] + Commands --> Card[cards/storeConfirmationCard.js] Card -->|confirm| Attachment[attachmentActions.js] - Attachment --> Flow[flows/buildStore.js] + Attachment --> Flow[flows/stageStore.js] Flow --> Locations[webex/locations.js] Flow --> Devices[webex/devices.js] Flow --> Announce[webex/announcements.js] diff --git a/src/cards/storeConfirmationCard.js b/src/cards/storeConfirmationCard.js index 3b1bc6a..5d474b3 100644 --- a/src/cards/storeConfirmationCard.js +++ b/src/cards/storeConfirmationCard.js @@ -1,8 +1,9 @@ /** - * Build the confirmation card shown before build/stage/migrate operations. - * `action` is what the "Yes" button posts back to the bot (buildStore, stageStore, migrateStore). + * Build the confirmation card shown before stage/finalize operations. + * `action` is what the "Yes" button posts back to the bot (e.g. "stageStore", + * "finalizeStore"). The attachmentAction handler switches on that value. */ -export function buildStoreInfoCard(storeInfo, userInfo, action) { +export function storeConfirmationCard(storeInfo, userInfo, action) { const storeFacts = [ { title: 'Name', value: storeInfo.name }, { title: 'Brand', value: storeInfo.brand }, @@ -52,13 +53,13 @@ export function buildStoreInfoCard(storeInfo, userInfo, action) { { type: 'Action.Submit', title: 'Yes', - id: 'btnBuildStore', + id: 'btnConfirm', data: { action, storeInfo, userInfo }, }, { type: 'Action.Submit', title: 'No', - id: 'btnBadInfo', + id: 'btnCancel', data: { action: 'deleteCard' }, }, ], diff --git a/src/commands/attachmentActions.js b/src/commands/attachmentActions.js index a33c1e3..3af12b2 100644 --- a/src/commands/attachmentActions.js +++ b/src/commands/attachmentActions.js @@ -1,7 +1,6 @@ import { logger } from '../logger.js'; -import { buildStoreLocation } from '../flows/buildStore.js'; import { stageStoreLocation } from '../flows/stageStore.js'; -import { migrateStoreLocation } from '../flows/migrateStore.js'; +import { finalizeStoreLocation } from '../flows/finalizeStore.js'; import { getWebexDeviceDetail } from '../webex/devices.js'; async function runFlow(bot, trigger, verb, fn) { @@ -36,14 +35,11 @@ export function register(framework) { const action = trigger.attachmentAction.inputs.action; switch (action) { - case 'buildStore': - await runFlow(bot, trigger, 'Building', buildStoreLocation); - break; case 'stageStore': await runFlow(bot, trigger, 'Staging', stageStoreLocation); break; - case 'migrateStore': - await runFlow(bot, trigger, 'Migrating store', migrateStoreLocation); + case 'finalizeStore': + await runFlow(bot, trigger, 'Finalizing', finalizeStoreLocation); break; case 'showDevice': await showDevices(bot, trigger); diff --git a/src/commands/buildStore.js b/src/commands/buildStore.js deleted file mode 100644 index 6ca27d8..0000000 --- a/src/commands/buildStore.js +++ /dev/null @@ -1,30 +0,0 @@ -import { logger } from '../logger.js'; -import { getStoreInfo } from '../integrations/siw.js'; -import { findWebexUser } from '../webex/users.js'; -import { buildStoreInfoCard } from '../cards/storeInfoCard.js'; -import { parseStoreNumber, requireAgent, storeEmail } from './helpers.js'; - -export function register(framework) { - framework.hears( - /\/buildstore/i, - async (bot, trigger) => { - logger.info(`${trigger.person.displayName} ran the buildStore command.`); - if (!requireAgent(bot)) return; - const storeNumber = parseStoreNumber(trigger); - if (!storeNumber) { - bot.say('Usage: `/buildStore ` — e.g. `/buildStore 499`'); - return; - } - try { - const storeInfo = await getStoreInfo(storeNumber); - const userInfo = await findWebexUser(storeEmail(storeNumber)); - const card = buildStoreInfoCard(storeInfo, userInfo, 'buildStore'); - bot.sendCard(card, 'Please use another client'); - } catch (error) { - logger.error('buildStore command failed:', error); - bot.say('markdown', `Error running /buildstore:\n\`\`\`\n${error.message}\n\`\`\``); - } - }, - '**/buildStore** - Builds a store location for Webex Calling (New and remodels).', - ); -} diff --git a/src/commands/finalizeStore.js b/src/commands/finalizeStore.js index 5f84ed8..659477b 100644 --- a/src/commands/finalizeStore.js +++ b/src/commands/finalizeStore.js @@ -1,34 +1,49 @@ import { logger } from '../logger.js'; import { getStoreInfo } from '../integrations/siw.js'; import { findWebexUser } from '../webex/users.js'; -import { buildStoreInfoCard } from '../cards/storeInfoCard.js'; +import { findWebexLocation } from '../webex/locations.js'; +import { storeConfirmationCard } from '../cards/storeConfirmationCard.js'; import { parseStoreNumber, requireAgent, storeEmail } from './helpers.js'; export function register(framework) { framework.hears( - /\/migratestore/i, + /\/finalizestore/i, async (bot, trigger) => { - logger.info(`${trigger.person.displayName} ran the migrateStore command.`); + logger.info(`${trigger.person.displayName} ran the finalizeStore command.`); if (!requireAgent(bot)) return; const storeNumber = parseStoreNumber(trigger); if (!storeNumber) { - bot.say('Usage: `/migrateStore ` — e.g. `/migrateStore 499`'); + bot.say('Usage: `/finalizeStore ` — e.g. `/finalizeStore 499`'); return; } try { const storeInfo = await getStoreInfo(storeNumber); - storeInfo.extension = `5${String(storeNumber).padStart(4, '0')}`; - const userInfo = await findWebexUser(storeEmail(storeNumber)); - const card = buildStoreInfoCard(storeInfo, userInfo, 'migrateStore'); + const [userInfo, locationMatches] = await Promise.all([ + findWebexUser(storeEmail(storeNumber)), + findWebexLocation(storeInfo.name), + ]); + // Preflight: if /stageStore was never run, the location won't + // exist in Webex, and every downstream mutation would fail. + // Bail out here with an actionable message instead of showing + // a confirmation card that's guaranteed to blow up on click. + if (!locationMatches.length) { + bot.say( + 'markdown', + `No Webex location found for **${storeInfo.name}**. ` + + `Run \`/stageStore ${storeNumber}\` first.`, + ); + return; + } + const card = storeConfirmationCard(storeInfo, userInfo, 'finalizeStore'); bot.sendCard(card, 'Please use another client'); } catch (error) { - logger.error('migrateStore command failed:', error); + logger.error('finalizeStore command failed:', error); bot.say( 'markdown', - `Error running /migratestore:\n\`\`\`\n${error.message}\n\`\`\``, + `Error running /finalizeStore:\n\`\`\`\n${error.message}\n\`\`\``, ); } }, - '**/migrateStore** - Completes the store migration for an open store.', + '**/finalizeStore** - Finalizes a staged store: attaches the phone number, sets caller ID, creates the auto-attendant, and cleans up licensing.', ); } diff --git a/src/commands/stageStore.js b/src/commands/stageStore.js index a3901d2..cd32fec 100644 --- a/src/commands/stageStore.js +++ b/src/commands/stageStore.js @@ -1,7 +1,7 @@ import { logger } from '../logger.js'; import { getStoreInfo } from '../integrations/siw.js'; import { findWebexUser } from '../webex/users.js'; -import { buildStoreInfoCard } from '../cards/storeInfoCard.js'; +import { storeConfirmationCard } from '../cards/storeConfirmationCard.js'; import { parseStoreNumber, requireAgent, storeEmail } from './helpers.js'; export function register(framework) { @@ -17,15 +17,14 @@ export function register(framework) { } try { const storeInfo = await getStoreInfo(storeNumber); - storeInfo.extension = `8${String(storeNumber).padStart(4, '0')}`; const userInfo = await findWebexUser(storeEmail(storeNumber)); - const card = buildStoreInfoCard(storeInfo, userInfo, 'stageStore'); + const card = storeConfirmationCard(storeInfo, userInfo, 'stageStore'); bot.sendCard(card, 'Please use another client'); } catch (error) { logger.error('stageStore command failed:', error); bot.say('markdown', `Error running /stagestore:\n\`\`\`\n${error.message}\n\`\`\``); } }, - '**/stageStore** - Stages a store location for Webex Calling (Pre-migration).', + '**/stageStore** - Stages a store location for Webex Calling (everything except the phone number).', ); } diff --git a/src/flows/buildStore.js b/src/flows/buildStore.js deleted file mode 100644 index 8688dcd..0000000 --- a/src/flows/buildStore.js +++ /dev/null @@ -1,73 +0,0 @@ -import { - addPhoneNumbersToLocation, - createLocation, - enableLocationForCalling, - updateInternalDialing, - updateLocationOutgoingPermission, - updateLocationRouteGroup, - updateLocationVoicemail, - updateLocationVoicePortal, - updateMusicOnHold, -} from '../webex/locations.js'; -import { scheduleStoreDeviceSettings } from '../webex/devices.js'; -import { createAllHoursSchedule } from '../webex/schedules.js'; -import { uploadGreeting } from '../webex/announcements.js'; -import { addWebexCallingToStoreUser, normalizeStoreUserLicenses } from '../webex/licensing.js'; -import { updateUserCallExperience } from '../webex/users.js'; -import { logger } from '../logger.js'; -import { runStep } from './stepRunner.js'; -import { greetingForBrand } from './greetingSelector.js'; - -/** - * "Build store" — full green-field provisioning: create location, calling, - * greeting, user attach, license cleanup. - */ -export async function buildStoreLocation(bot, locationInfo, userInfo) { - logger.info(`Building location ${locationInfo.name}`); - const location = await createLocation(locationInfo); - bot.say( - 'markdown', - `
Created location ${location.name}.
`, - ); - - await runStep( - bot, - 'Enabled location for Webex Calling', - () => enableLocationForCalling(location), - { critical: true }, - ); - await runStep(bot, 'Updated location Webex Calling connection', () => - updateLocationRouteGroup(location), - ); - await runStep(bot, 'Added phone numbers to location', () => - addPhoneNumbersToLocation(location, locationInfo.phoneNumber), - ); - await runStep(bot, 'Updated internal dialing', () => updateInternalDialing(location)); - await runStep(bot, 'Updated location outgoing permission', () => - updateLocationOutgoingPermission(location), - ); - await runStep(bot, 'Updated music on hold', () => updateMusicOnHold(location)); - await runStep(bot, 'Updated location voicemail', () => updateLocationVoicemail(location)); - await runStep(bot, 'Updated location voice portal', () => - updateLocationVoicePortal(location, locationInfo.vpExtension), - ); - await runStep(bot, 'Created a schedule', () => createAllHoursSchedule(location)); - await runStep(bot, 'Scheduled device changes', () => scheduleStoreDeviceSettings(location)); - - const { file, fileName } = greetingForBrand(locationInfo); - const greeting = await runStep(bot, 'Succeeded uploading greeting', () => - uploadGreeting(location, file, fileName), - ); - if (greeting) location.announcementId = greeting.id; - - await runStep(bot, 'Updated store user', () => - addWebexCallingToStoreUser(location, userInfo, locationInfo.extension), - ); - await runStep(bot, 'Updated user call application experience', () => - updateUserCallExperience(userInfo), - ); - await runStep(bot, 'Fixed Licenses', () => normalizeStoreUserLicenses(userInfo)); - - bot.say('Build Complete!'); - return location; -} diff --git a/src/flows/finalizeStore.js b/src/flows/finalizeStore.js index b81b25e..3d020e3 100644 --- a/src/flows/finalizeStore.js +++ b/src/flows/finalizeStore.js @@ -17,16 +17,21 @@ import { logger } from '../logger.js'; import { runStep } from './stepRunner.js'; /** - * "Migrate store" — finish the cut-over for a store that was previously staged. + * "Finalize store" — finish the cut-over for a store that was previously staged. * Attach the phone number, set caller ID, create the auto-attendant, and clean * up the user's licensing/voicemail. + * + * Precondition: the location already exists in Webex (i.e. `/stageStore` has + * been run). The command handler preflight rejects unstaged stores before we + * get here, but we defensively re-check because the flow can also be invoked + * directly from the attachmentAction submission. */ -export async function migrateStoreLocation(bot, locationInfo, userInfo) { - logger.info(`Migrating location ${locationInfo.name}`); +export async function finalizeStoreLocation(bot, locationInfo, userInfo) { + logger.info(`Finalizing location ${locationInfo.name}`); const matches = await findWebexLocation(locationInfo.name); if (!matches.length) { throw new Error( - `No existing Webex location found for ${locationInfo.name}. Did you stage first?`, + `No existing Webex location found for ${locationInfo.name}. Run /stageStore first.`, ); } const location = matches[0]; @@ -70,6 +75,6 @@ export async function migrateStoreLocation(bot, locationInfo, userInfo) { await runStep(bot, 'Fixed Licenses', () => normalizeStoreUserLicenses(userInfo)); - bot.say('Migration Complete!'); + bot.say('markdown', `**Finalize complete for ${locationInfo.name}.** Location is now live.`); return location; } diff --git a/src/flows/stageStore.js b/src/flows/stageStore.js index 7b64c70..d27ef05 100644 --- a/src/flows/stageStore.js +++ b/src/flows/stageStore.js @@ -55,6 +55,11 @@ export async function stageStoreLocation(bot, locationInfo, userInfo) { addWebexCallingToStoreUser(location, userInfo, locationInfo.extension), ); - bot.say('Staging Complete!'); + bot.say( + 'markdown', + `**Staging complete for ${locationInfo.name}.** ` + + `Next: purchase a phone number in Webex Control Hub, then run ` + + `\`/finalizeStore ${locationInfo.storeNumber}\`.`, + ); return location; } diff --git a/src/index.js b/src/index.js index 1584629..ad04329 100644 --- a/src/index.js +++ b/src/index.js @@ -4,9 +4,8 @@ import { config } from './config.js'; import { logger } from './logger.js'; import { isAccessTokenExpiring, refreshAccessToken } from './webex/auth.js'; import { startWebSocketServer, stopWebSocketServer } from './services/websocket.js'; -import { register as registerBuildStore } from './commands/buildStore.js'; import { register as registerStageStore } from './commands/stageStore.js'; -import { register as registerMigrateStore } from './commands/migrateStore.js'; +import { register as registerFinalizeStore } from './commands/finalizeStore.js'; import { register as registerStoreInfo } from './commands/storeInfo.js'; import { register as registerUserInfo } from './commands/userInfo.js'; import { register as registerAttachmentActions } from './commands/attachmentActions.js'; @@ -21,9 +20,8 @@ framework.on('log', (msg) => { logger.info(msg); }); -registerBuildStore(framework); registerStageStore(framework); -registerMigrateStore(framework); +registerFinalizeStore(framework); registerStoreInfo(framework); registerUserInfo(framework); registerAttachmentActions(framework); diff --git a/src/webex/deviceCustomizations.js b/src/webex/deviceCustomizations.js index 634b55f..7e2d0e0 100644 --- a/src/webex/deviceCustomizations.js +++ b/src/webex/deviceCustomizations.js @@ -1,5 +1,5 @@ // Per-device-family calling customizations applied to every store location -// as part of buildStore / stageStore. Sent verbatim to +// as part of stageStore. Sent verbatim to // POST /telephony/config/jobs/devices/callDeviceSettings under `customizations`. // // This is intentionally a data-only module so device config changes are a diff --git a/src/webex/locations.js b/src/webex/locations.js index c90433d..7226fad 100644 --- a/src/webex/locations.js +++ b/src/webex/locations.js @@ -65,17 +65,6 @@ export async function addPhoneNumbersToLocation(location, phoneNumber) { ); } -/** - * Point the location at its route group (used before phone numbers exist). - */ -export async function updateLocationRouteGroup(location) { - const routeGroup = routeGroupFor(location); - const body = { - connection: { id: routeGroup.id, type: 'ROUTE_GROUP' }, - }; - return webexJson('PUT', `/telephony/config/locations/${encodeURIComponent(location.id)}`, body); -} - /** * Set the location's caller-ID/external-caller-ID after phone numbers exist. */