From 1946c373d7ff6d217790de73d1ac09454745fb85 Mon Sep 17 00:00:00 2001 From: Muhsin Date: Fri, 27 Mar 2026 14:16:17 +0400 Subject: [PATCH] chore: teardown Twilio voice resources when disabled --- .../settingsPage/VoiceConfigurationPage.vue | 6 ++++ .../models/enterprise/channel/twilio_sms.rb | 18 ++++++++++ .../models/channel/twilio_sms_voice_spec.rb | 35 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue index c8bc3124d..cf954c353 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue @@ -23,6 +23,7 @@ export default { voiceEnabled: this.inbox.voice_enabled || false, apiKeySid: this.inbox.api_key_sid || '', apiKeySecret: '', + isUpdating: false, }; }, computed: { @@ -57,6 +58,7 @@ export default { }, methods: { async updateVoiceSettings() { + this.isUpdating = true; try { const channelPayload = { voice_enabled: this.voiceEnabled }; @@ -72,9 +74,12 @@ export default { formData: false, channel: channelPayload, }); + this.apiKeySecret = ''; useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); } catch (error) { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } finally { + this.isUpdating = false; } }, }, @@ -135,6 +140,7 @@ export default {
diff --git a/enterprise/app/models/enterprise/channel/twilio_sms.rb b/enterprise/app/models/enterprise/channel/twilio_sms.rb index d58a87bf9..07b6a9d04 100644 --- a/enterprise/app/models/enterprise/channel/twilio_sms.rb +++ b/enterprise/app/models/enterprise/channel/twilio_sms.rb @@ -8,6 +8,7 @@ module Enterprise::Channel::TwilioSms validate :voice_requires_phone_number, if: :voice_enabled? before_validation :provision_twiml_app, on: :create, if: :voice_enabled? before_validation :provision_twiml_app_on_update, on: :update, if: :voice_enabled_changed_to_true? + after_update :teardown_voice, if: :voice_disabled? end end @@ -56,6 +57,23 @@ module Enterprise::Channel::TwilioSms voice_enabled? && voice_enabled_changed? end + def voice_disabled? + !voice_enabled? && voice_enabled_previously_changed? + end + + def teardown_voice + delete_twiml_app if twiml_app_sid.present? + rescue StandardError => e + Rails.logger.error("TWILIO_VOICE_TEARDOWN_ERROR: #{e.class} #{e.message} phone=#{phone_number} account=#{account_id}") + ensure + update_columns(twiml_app_sid: nil, api_key_secret: nil) + end + + def delete_twiml_app + twilio_client = Twilio::REST::Client.new(account_sid, auth_token) + twilio_client.applications(twiml_app_sid).delete + end + def provision_twiml_app service = ::Twilio::VoiceWebhookSetupService.new(channel: self) self.twiml_app_sid = service.perform diff --git a/spec/enterprise/models/channel/twilio_sms_voice_spec.rb b/spec/enterprise/models/channel/twilio_sms_voice_spec.rb index 55134ab59..50b484e58 100644 --- a/spec/enterprise/models/channel/twilio_sms_voice_spec.rb +++ b/spec/enterprise/models/channel/twilio_sms_voice_spec.rb @@ -60,4 +60,39 @@ RSpec.describe Channel::TwilioSms do expect(channel.twiml_app_sid).to eq(twiml_app_sid) end end + + describe 'teardown on disable' do + let(:channel) { create(:channel_twilio_sms, :with_voice, account: account) } + let(:app_context) { double('app_context') } + let(:twilio_client) { instance_double(Twilio::REST::Client) } + + before do + allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client) + allow(twilio_client).to receive(:applications).with(channel.twiml_app_sid).and_return(app_context) + allow(app_context).to receive(:delete) + end + + it 'deletes the TwiML app and clears voice credentials' do + original_twiml_sid = channel.twiml_app_sid + channel.update!(voice_enabled: false) + + expect(twilio_client).to have_received(:applications).with(original_twiml_sid) + expect(app_context).to have_received(:delete) + expect(channel.reload.twiml_app_sid).to be_nil + expect(channel.reload.api_key_secret).to be_nil + end + + it 'preserves api_key_sid' do + channel.update!(voice_enabled: false) + expect(channel.reload.api_key_sid).to be_present + end + + it 'does not fail if Twilio API errors' do + allow(app_context).to receive(:delete).and_raise(Twilio::REST::RestError.new('Not found', double(status_code: 404, body: {}))) + + expect { channel.update!(voice_enabled: false) }.not_to raise_error + expect(channel.reload.twiml_app_sid).to be_nil + expect(channel.reload.api_key_secret).to be_nil + end + end end