feat(voice): Improved voice call creation flow [EE] (#12268)

This PR improves the voice call creation flow by simplifying
configuration and automating setup with Twilio APIs.

references: #11602 , #11481 

## Key changes

- Removed the requirement for twiml_app_sid – provisioning is now
automated through APIs.
- Auto-configured webhook URLs for:
  - Voice number callbacks
  - Status callbacks
  -  twiML callbacks
- Disabled business hours, help center, and related options until voice
inbox is fully supported.
- Added a configuration tab in the voice inbox to display the required
Twilio URLs (to make verification easier in Twilio console).


## Test Cases
- Provisioning
- Create a new voice inbox → verify that Twilio app provisioning happens
automatically.
  - Verify twiML callback 
- Webhook configuration
- Check that both voice number callback and status callback URLs are
auto-populated in Twilio.
- Disabled features
- Confirm that business hours and help center options are
hidden/disabled for voice inbox.
- Configuration tab
- Open the voice inbox configuration tab → verify that the displayed
Twilio URLs match what’s set in Twilio.
This commit is contained in:
Sojan Jose
2025-08-22 13:38:23 +02:00
committed by GitHub
parent d35b0a9465
commit d48503bdcf
12 changed files with 302 additions and 39 deletions

View File

@@ -24,6 +24,9 @@ RSpec.describe 'Enterprise Inboxes API', type: :request do
end
it 'creates a voice inbox when administrator' do
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(instance_double(Twilio::VoiceWebhookSetupService,
perform: "AP#{SecureRandom.hex(16)}"))
post "/api/v1/accounts/#{account.id}/inboxes",
headers: admin.create_new_auth_token,
params: { name: 'Voice Inbox',
@@ -31,7 +34,8 @@ RSpec.describe 'Enterprise Inboxes API', type: :request do
provider_config: { account_sid: "AC#{SecureRandom.hex(16)}",
auth_token: SecureRandom.hex(16),
api_key_sid: SecureRandom.hex(8),
api_key_secret: SecureRandom.hex(16) } } },
api_key_secret: SecureRandom.hex(16),
twiml_app_sid: "AP#{SecureRandom.hex(16)}" } } },
as: :json
expect(response).to have_http_status(:success)

View File

@@ -3,8 +3,13 @@
require 'rails_helper'
RSpec.describe Channel::Voice do
let(:twiml_app_sid) { 'AP1234567890abcdef' }
let(:channel) { create(:channel_voice) }
before do
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(instance_double(Twilio::VoiceWebhookSetupService, perform: twiml_app_sid))
end
it 'has a valid factory' do
expect(channel).to be_valid
end
@@ -40,12 +45,19 @@ RSpec.describe Channel::Voice do
expect(channel.errors[:provider_config]).to include('api_key_secret is required for Twilio provider')
end
it 'validates presence of twiml_app_sid in provider_config' do
channel.provider_config = { account_sid: 'sid', auth_token: 'token', api_key_sid: 'key', api_key_secret: 'secret' }
expect(channel).not_to be_valid
expect(channel.errors[:provider_config]).to include('twiml_app_sid is required for Twilio provider')
end
it 'is valid with all required provider_config fields' do
channel.provider_config = {
account_sid: 'test_sid',
auth_token: 'test_token',
api_key_sid: 'test_key',
api_key_secret: 'test_secret'
api_key_secret: 'test_secret',
twiml_app_sid: 'test_app_sid'
}
expect(channel).to be_valid
end
@@ -57,4 +69,11 @@ RSpec.describe Channel::Voice do
expect(channel.name).to include(channel.phone_number)
end
end
describe 'provisioning on create' do
it 'stores twiml_app_sid in provider_config' do
ch = create(:channel_voice)
expect(ch.provider_config.with_indifferent_access[:twiml_app_sid]).to eq(twiml_app_sid)
end
end
end

View File

@@ -0,0 +1,88 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Twilio::VoiceWebhookSetupService do
let(:account_sid) { 'ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
let(:auth_token) { 'auth_token_123' }
let(:api_key_sid) { 'SKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }
let(:api_key_secret) { 'api_key_secret_123' }
let(:phone_number) { '+15551230001' }
let(:frontend_url) { 'https://app.chatwoot.test' }
let(:channel) do
build(:channel_voice, phone_number: phone_number, provider_config: {
account_sid: account_sid,
auth_token: auth_token,
api_key_sid: api_key_sid,
api_key_secret: api_key_secret
})
end
let(:twilio_base_url) { "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}" }
let(:incoming_numbers_url) { "#{twilio_base_url}/IncomingPhoneNumbers.json" }
let(:applications_url) { "#{twilio_base_url}/Applications.json" }
let(:phone_number_sid) { 'PN123' }
let(:phone_number_url) { "#{twilio_base_url}/IncomingPhoneNumbers/#{phone_number_sid}.json" }
before do
# Token validation using Account SID + Auth Token
stub_request(:get, /#{Regexp.escape(incoming_numbers_url)}.*/)
.with(basic_auth: [account_sid, auth_token])
.to_return(status: 200,
body: { incoming_phone_numbers: [], meta: { key: 'incoming_phone_numbers' } }.to_json,
headers: { 'Content-Type' => 'application/json' })
# Number lookup using API Key SID/Secret
stub_request(:get, /#{Regexp.escape(incoming_numbers_url)}.*/)
.with(basic_auth: [api_key_sid, api_key_secret])
.to_return(status: 200,
body: { incoming_phone_numbers: [{ sid: phone_number_sid }], meta: { key: 'incoming_phone_numbers' } }.to_json,
headers: { 'Content-Type' => 'application/json' })
# TwiML App create (voice only)
stub_request(:post, applications_url)
.with(basic_auth: [api_key_sid, api_key_secret])
.to_return(status: 201,
body: { sid: 'AP123' }.to_json,
headers: { 'Content-Type' => 'application/json' })
# Incoming Phone Number webhook update
stub_request(:post, phone_number_url)
.with(basic_auth: [api_key_sid, api_key_secret])
.to_return(status: 200,
body: { sid: phone_number_sid }.to_json,
headers: { 'Content-Type' => 'application/json' })
end
it 'creates a TwiML App and configures number webhooks with correct URLs' do
with_modified_env FRONTEND_URL: frontend_url do
service = described_class.new(channel: channel)
sid = service.perform
expect(sid).to eq('AP123')
expected_voice_url = channel.voice_call_webhook_url
expected_status_url = channel.voice_status_webhook_url
# Assert TwiML App creation body includes voice URL and POST method
expect(
a_request(:post, applications_url)
.with(body: hash_including('VoiceUrl' => expected_voice_url, 'VoiceMethod' => 'POST'))
).to have_been_made
# Assert number webhook update body includes both URLs and POST methods
expect(
a_request(:post, phone_number_url)
.with(
body: hash_including(
'VoiceUrl' => expected_voice_url,
'VoiceMethod' => 'POST',
'StatusCallback' => expected_status_url,
'StatusCallbackMethod' => 'POST'
)
)
).to have_been_made
end
end
end