refactor: move whatsapp calling backend to enterprise edition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tanmay Deep Sharma
2026-03-19 18:00:34 +05:30
co-authored by Claude Opus 4.6
parent f120acbd9f
commit 84fd4588cc
18 changed files with 82 additions and 44 deletions
+2
View File
@@ -44,6 +44,7 @@ export const FEATURE_FLAGS = {
COMPANIES: 'companies',
ADVANCED_SEARCH: 'advanced_search',
CONVERSATION_REQUIRED_ATTRIBUTES: 'conversation_required_attributes',
WHATSAPP_CALL: 'whatsapp_call',
};
export const PREMIUM_FEATURES = [
@@ -55,4 +56,5 @@ export const PREMIUM_FEATURES = [
FEATURE_FLAGS.SAML,
FEATURE_FLAGS.CONVERSATION_REQUIRED_ATTRIBUTES,
FEATURE_FLAGS.ADVANCED_ASSIGNMENT,
FEATURE_FLAGS.WHATSAPP_CALL,
];
+3 -35
View File
@@ -1,5 +1,5 @@
class Webhooks::WhatsappEventsJob < ApplicationJob
queue_as :default
queue_as :low
def perform(params = {})
channel = find_channel_from_whatsapp_business_payload(params)
@@ -59,16 +59,6 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
end
def handle_message_events(channel, params)
if call_event?(params)
handle_call_events(channel, params)
return
end
if call_permission_reply?(params)
handle_call_permission_reply(channel, params)
return
end
case channel.provider
when 'whatsapp_cloud'
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
@@ -77,30 +67,6 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
end
end
def handle_call_events(channel, params)
Whatsapp::IncomingCallService.new(
inbox: channel.inbox,
params: extract_call_params(params)
).perform
end
def call_event?(params)
params.dig(:entry, 0, :changes, 0, :field) == 'calls'
end
def call_permission_reply?(params)
message = params.dig(:entry, 0, :changes, 0, :value, :messages, 0)
message&.dig(:type) == 'interactive' && message&.dig(:interactive, :type) == 'call_permission_reply'
end
def handle_call_permission_reply(channel, params)
Whatsapp::CallPermissionReplyService.new(inbox: channel.inbox, params: params).perform
end
def extract_call_params(params)
params.dig(:entry, 0, :changes, 0, :value) || {}
end
private
def channel_is_inactive?(channel)
@@ -134,3 +100,5 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
end
end
Webhooks::WhatsappEventsJob.prepend_mod_with('Webhooks::WhatsappEventsJob')
-1
View File
@@ -133,7 +133,6 @@ class Account < ApplicationRecord
has_many :twitter_profiles, dependent: :destroy_async, class_name: '::Channel::TwitterProfile'
has_many :users, through: :account_users
has_many :web_widgets, dependent: :destroy_async, class_name: '::Channel::WebWidget'
has_many :whatsapp_calls, dependent: :destroy_async
has_many :webhooks, dependent: :destroy_async
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
has_many :working_hours, dependent: :destroy_async
+7 -1
View File
@@ -86,7 +86,7 @@ class Whatsapp::FacebookApiClient
body: {
override_callback_uri: callback_url,
verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes calls]
subscribed_fields: webhook_subscribed_fields
}.to_json
)
@@ -102,6 +102,10 @@ class Whatsapp::FacebookApiClient
handle_response(response, 'Webhook unsubscription failed')
end
def webhook_subscribed_fields
%w[messages smb_message_echoes]
end
private
def request_headers
@@ -123,3 +127,5 @@ class Whatsapp::FacebookApiClient
response.parsed_response
end
end
Whatsapp::FacebookApiClient.prepend_mod_with('Whatsapp::FacebookApiClient')
@@ -1,6 +1,4 @@
class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService
include Whatsapp::Providers::WhatsappCloudCallMethods
def send_message(phone_number, message)
@message = message
@@ -207,3 +205,5 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
process_response(response, message)
end
end
Whatsapp::Providers::WhatsappCloudService.include_mod_with('Whatsapp::Providers::WhatsappCloudService')
+3 -3
View File
@@ -104,10 +104,10 @@
display_name: Audit Logs
enabled: false
premium: true
- name: response_bot
display_name: Response Bot
- name: whatsapp_call
display_name: WhatsApp Calling
enabled: false
deprecated: true
premium: true
- name: message_reply_to
display_name: Message Reply To
enabled: false
@@ -1,4 +1,5 @@
class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseController
before_action :ensure_whatsapp_call_enabled
before_action :set_whatsapp_call, only: [:accept, :reject, :terminate]
def accept
@@ -85,6 +86,10 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
nil
end
def ensure_whatsapp_call_enabled
render_payment_required('WhatsApp calling is not enabled for this account') unless current_account.feature_enabled?('whatsapp_call')
end
def set_whatsapp_call
@whatsapp_call = current_account.whatsapp_calls.find(params[:id])
authorize @whatsapp_call.conversation, :show?
@@ -0,0 +1,41 @@
module Enterprise::Webhooks::WhatsappEventsJob
def handle_message_events(channel, params)
if call_event?(params)
handle_call_events(channel, params)
return
end
if call_permission_reply?(params)
handle_call_permission_reply(channel, params)
return
end
super
end
private
def call_event?(params)
params.dig(:entry, 0, :changes, 0, :field) == 'calls'
end
def call_permission_reply?(params)
message = params.dig(:entry, 0, :changes, 0, :value, :messages, 0)
message&.dig(:type) == 'interactive' && message&.dig(:interactive, :type) == 'call_permission_reply'
end
def handle_call_events(channel, params)
Whatsapp::IncomingCallService.new(
inbox: channel.inbox,
params: extract_call_params(params)
).perform
end
def handle_call_permission_reply(channel, params)
Whatsapp::CallPermissionReplyService.new(inbox: channel.inbox, params: params).perform
end
def extract_call_params(params)
params.dig(:entry, 0, :changes, 0, :value) || {}
end
end
@@ -17,6 +17,7 @@ module Enterprise::Concerns::Account
has_many :copilot_threads, dependent: :destroy_async
has_many :companies, dependent: :destroy_async
has_many :voice_channels, dependent: :destroy_async, class_name: '::Channel::Voice'
has_many :whatsapp_calls, dependent: :destroy_async
has_one :saml_settings, dependent: :destroy_async, class_name: 'AccountSamlSettings'
end
@@ -0,0 +1,5 @@
module Enterprise::Whatsapp::FacebookApiClient
def webhook_subscribed_fields
super + %w[calls]
end
end
@@ -0,0 +1,7 @@
module Enterprise::Whatsapp::Providers::WhatsappCloudService
extend ActiveSupport::Concern
included do
include ::Whatsapp::Providers::WhatsappCloudCallMethods
end
end
@@ -2,6 +2,8 @@ class Whatsapp::CallPermissionReplyService
pattr_initialize [:inbox!, :params!]
def perform
return unless inbox.account.feature_enabled?('whatsapp_call')
reply_data = extract_reply_data
return unless reply_data&.dig(:accepted)
@@ -2,6 +2,8 @@ class Whatsapp::IncomingCallService
pattr_initialize [:inbox!, :params!]
def perform
return unless inbox.account.feature_enabled?('whatsapp_call')
calls = params[:calls]
return if calls.blank?
@@ -177,7 +177,7 @@ describe Whatsapp::FacebookApiClient do
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
subscribed_fields: %w[messages smb_message_echoes] }.to_json
)
.to_return(
status: 200,
@@ -224,7 +224,7 @@ describe Whatsapp::FacebookApiClient do
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes calls] }.to_json
subscribed_fields: %w[messages smb_message_echoes] }.to_json
)
.to_return(status: 400, body: { error: 'Webhook callback override failed' }.to_json)
end