Files
0e122188e9 feat: Add voice calling as a capability on Twilio SMS channel(Enterprise) (#13963)
Voice calling is now a capability on the existing TwilioSms rather than
a separate Voice model. A single Twilio phone number handles both SMS
and voice calls through one inbox.
Fixes
https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel
and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms

**What changed**
- Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms
- Added voice_enabled, twiml_app_sid, api_key_secret columns to
channel_twilio_sms table
  - Dropped channel_voice table (no production data)
- All voice logic lives in Enterprise layer via
prepend_mod_with('Channel::TwilioSms')
- Added Voice settings tab on Twilio SMS inbox settings to
enable/disable voice
  - Validates Twilio number voice capability before provisioning
- Teardown service cleans up TwiML app and credentials when voice is
disabled
- Frontend voice detection uses isVoiceCallEnabled() /
getVoiceCallProvider() helpers — extensible to future providers
  - Gated by channel_voice feature flag

**How to test**

1. Enable feature flag:
Account.find(<id>).enable_features('channel_voice')
2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials →
verify incoming/outgoing calls and SMS work
3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox
→ Voice tab → toggle on → provide API key credentials → verify calls
work
4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted,
credentials cleared, SMS still works
5. Re-enable voice: Toggle on again → must provide api_key_secret again
→ new TwiML app provisioned

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-29 11:32:19 +04:00

44 lines
2.3 KiB
Ruby

require 'rails_helper'
describe Voice::Provider::Twilio::Adapter do
let(:account) { create(:account) }
let(:channel) { create(:channel_twilio_sms, :with_voice, account: account) }
let(:adapter) { described_class.new(channel) }
let(:webhook_service) { instance_double(Twilio::VoiceWebhookSetupService, perform: true) }
let(:calls_double) { instance_double(Twilio::REST::Api::V2010::AccountContext::CallList) }
let(:call_instance) do
instance_double(Twilio::REST::Api::V2010::AccountContext::CallInstance, sid: 'CA123', status: 'queued')
end
let(:client_double) { instance_double(Twilio::REST::Client, calls: calls_double) }
before do
allow(Twilio::VoiceWebhookSetupService).to receive(:new).and_return(webhook_service)
end
it 'initiates an outbound call with expected params' do
allow(calls_double).to receive(:create).and_return(call_instance)
allow(Twilio::REST::Client).to receive(:new)
.with(channel.account_sid, channel.auth_token)
.and_return(client_double)
result = adapter.initiate_call(to: '+15550001111', conference_sid: 'CF999', agent_id: 42)
phone_digits = channel.phone_number.delete_prefix('+')
expected_url = Rails.application.routes.url_helpers.twilio_voice_call_url(phone: phone_digits)
expected_status_callback = Rails.application.routes.url_helpers.twilio_voice_status_url(phone: phone_digits)
expect(calls_double).to have_received(:create).with(hash_including(
from: channel.phone_number,
to: '+15550001111',
url: expected_url,
status_callback: expected_status_callback,
status_callback_event: array_including('completed', 'failed', 'busy', 'no-answer',
'canceled')
))
expect(result[:call_sid]).to eq('CA123')
expect(result[:conference_sid]).to eq('CF999')
expect(result[:agent_id]).to eq(42)
expect(result[:call_direction]).to eq('outbound')
end
end