Files
chatwoot/enterprise/app/services/twilio/voice_webhook_setup_service.rb
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

84 lines
2.4 KiB
Ruby

class Twilio::VoiceWebhookSetupService
include Rails.application.routes.url_helpers
pattr_initialize [:channel!]
HTTP_METHOD = 'POST'.freeze
# Returns created TwiML App SID on success.
def perform
validate_token_credentials!
app_sid = create_twiml_app!
configure_number_webhooks!
app_sid
end
private
def validate_token_credentials!
channel.client.incoming_phone_numbers.list(limit: 1)
rescue StandardError => e
log_twilio_error('AUTH_VALIDATION_TOKEN', e)
raise
end
def create_twiml_app!
friendly_name = "Chatwoot Voice #{channel.phone_number}"
app = channel.client.applications.create(
friendly_name: friendly_name,
voice_url: channel.voice_call_webhook_url,
voice_method: HTTP_METHOD
)
app.sid
rescue StandardError => e
log_twilio_error('TWIML_APP_CREATE', e)
raise
end
def configure_number_webhooks!
numbers = channel.client.incoming_phone_numbers.list(phone_number: channel.phone_number)
if numbers.empty?
Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
return
end
channel.client
.incoming_phone_numbers(numbers.first.sid)
.update(
voice_url: channel.voice_call_webhook_url,
voice_method: HTTP_METHOD,
status_callback: channel.voice_status_webhook_url,
status_callback_method: HTTP_METHOD
)
rescue StandardError => e
log_twilio_error('NUMBER_WEBHOOKS_UPDATE', e)
raise
end
def log_twilio_error(context, error)
details = build_error_details(context, error)
add_twilio_specific_details(details, error)
backtrace = error.backtrace.is_a?(Array) ? error.backtrace.first(5) : []
Rails.logger.error("TWILIO_VOICE_SETUP_ERROR: #{details} backtrace=#{backtrace}")
end
def build_error_details(context, error)
{
context: context,
phone_number: channel.phone_number,
account_sid: channel.account_sid,
error_class: error.class.to_s,
message: error.message
}
end
def add_twilio_specific_details(details, error)
details[:status_code] = error.status_code if error.respond_to?(:status_code)
details[:twilio_code] = error.code if error.respond_to?(:code)
details[:more_info] = error.more_info if error.respond_to?(:more_info)
details[:details] = error.details if error.respond_to?(:details)
end
end