From fcc13a50645310be3108b297fed9b436bd726626 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:07:44 +0700 Subject: [PATCH] add feature flag for wa es --- .../whatsapp/authorizations_controller.rb | 11 + .../settings/inbox/channels/Whatsapp.vue | 25 +- .../whatsapp/embedded_signup_service.rb | 2 - config/features.yml | 3 + .../authorizations_controller_spec.rb | 279 ++++++++++++++++++ spec/models/channel/whatsapp_spec.rb | 81 +++++ spec/models/concerns/featurable_spec.rb | 57 ++++ .../whatsapp/channel_creation_service_spec.rb | 7 +- .../whatsapp/embedded_signup_service_spec.rb | 10 +- 9 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb create mode 100644 spec/models/concerns/featurable_spec.rb diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb index 23572cf28..e7a1f3fa6 100644 --- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb @@ -1,4 +1,6 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController + before_action :validate_feature_enabled! + # POST /api/v1/accounts/:account_id/whatsapp/authorization # Handles the embedded signup callback data from the Facebook SDK def create @@ -40,6 +42,15 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts: }, status: :unprocessable_entity end + def validate_feature_enabled! + return if Current.account.feature_whatsapp_embedded_signup? + + render json: { + success: false, + error: 'WhatsApp embedded signup is not enabled for this account' + }, status: :forbidden + end + def validate_embedded_signup_params! missing_params = [] missing_params << 'code' if params[:code].blank? diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue index fd7eb2db7..d9c96ccd8 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue @@ -2,6 +2,7 @@ import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { useStore } from 'vuex'; import Twilio from './Twilio.vue'; import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue'; import CloudWhatsapp from './CloudWhatsapp.vue'; @@ -12,6 +13,7 @@ import twilioIcon from 'dashboard/assets/images/twilio.png'; const route = useRoute(); const router = useRouter(); const { t } = useI18n(); +const store = useStore(); const PROVIDER_TYPES = { WHATSAPP: 'whatsapp', @@ -28,6 +30,14 @@ const hasWhatsappAppId = computed(() => { ); }); +const isWhatsappEmbeddedSignupEnabled = computed(() => { + const accountId = route.params.accountId; + return store.getters['accounts/isFeatureEnabledonAccount']( + accountId, + 'whatsapp_embedded_signup' + ); +}); + const selectedProvider = computed(() => route.query.provider); const showProviderSelection = computed(() => !selectedProvider.value); @@ -58,6 +68,11 @@ const selectProvider = providerValue => { }; const shouldShowEmbeddedSignup = provider => { + // Check if the feature is enabled for the account + if (!isWhatsappEmbeddedSignupEnabled.value) { + return false; + } + return ( (provider === PROVIDER_TYPES.WHATSAPP && hasWhatsappAppId.value) || provider === PROVIDER_TYPES.WHATSAPP_EMBEDDED @@ -65,7 +80,15 @@ const shouldShowEmbeddedSignup = provider => { }; const shouldShowCloudWhatsapp = provider => { - return provider === PROVIDER_TYPES.WHATSAPP && !hasWhatsappAppId.value; + // If embedded signup feature is enabled and app ID is configured, don't show cloud whatsapp + if (isWhatsappEmbeddedSignupEnabled.value && hasWhatsappAppId.value) { + return false; + } + + // Show cloud whatsapp when: + // 1. Provider is whatsapp AND + // 2. Either no app ID is configured OR embedded signup feature is disabled + return provider === PROVIDER_TYPES.WHATSAPP; }; diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb index e72880944..0adf073cd 100644 --- a/app/services/whatsapp/embedded_signup_service.rb +++ b/app/services/whatsapp/embedded_signup_service.rb @@ -10,8 +10,6 @@ class Whatsapp::EmbeddedSignupService def perform validate_parameters! - GlobalConfig.clear_cache - # Exchange code for user access token access_token = Whatsapp::TokenExchangeService.new(@code).perform diff --git a/config/features.yml b/config/features.yml index 5171b1c01..36a84f4a6 100644 --- a/config/features.yml +++ b/config/features.yml @@ -176,3 +176,6 @@ - name: notion_integration display_name: Notion Integration enabled: false +- name: whatsapp_embedded_signup + display_name: WhatsApp Embedded Signup + enabled: false diff --git a/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb new file mode 100644 index 000000000..ab279a131 --- /dev/null +++ b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb @@ -0,0 +1,279 @@ +require 'rails_helper' + +RSpec.describe Api::V1::Accounts::Whatsapp::AuthorizationsController, type: :controller do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + + describe 'POST #create' do + context 'when user is not authenticated' do + it 'returns unauthorized' do + post :create, params: { account_id: account.id } + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when user is authenticated' do + before { sign_in(agent) } + + context 'when feature is not enabled' do + before do + account.disable_features!(:whatsapp_embedded_signup) + end + + it 'returns forbidden' do + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body['error']).to eq('WhatsApp embedded signup is not enabled for this account') + end + end + + context 'when feature is enabled' do + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'returns unprocessable entity when code is missing' do + post :create, params: { + account_id: account.id, + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('code') + end + + it 'returns unprocessable entity when business_id is missing' do + post :create, params: { + account_id: account.id, + code: 'test_code', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('business_id') + end + + it 'returns unprocessable entity when waba_id is missing' do + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to include('waba_id') + end + + it 'creates whatsapp channel successfully' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service) + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service to prevent HTTP calls + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + } + + expect(response).to have_http_status(:success) + response_data = response.parsed_body + expect(response_data['success']).to be true + expect(response_data['id']).to eq(inbox.id) + expect(response_data['name']).to eq(inbox.name) + expect(response_data['channel_type']).to eq('whatsapp') + end + + it 'calls the embedded signup service with correct parameters' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + expect(Whatsapp::EmbeddedSignupService).to receive(:new).with( + account: account, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + ).and_return(embedded_signup_service) + + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: 'test_phone_id' + } + end + + it 'accepts phone_number_id as optional parameter' do + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + + expect(Whatsapp::EmbeddedSignupService).to receive(:new).with( + account: account, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id', + phone_number_id: nil + ).and_return(embedded_signup_service) + + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:success) + end + + it 'returns unprocessable entity when service fails' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error') + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + response_data = response.parsed_body + expect(response_data['success']).to be false + expect(response_data['error']).to eq('Service error') + end + + it 'logs error when service fails' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error') + + expect(Rails.logger).to receive(:error).with(/\[WHATSAPP AUTHORIZATION\] Embedded signup error: Service error/) + expect(Rails.logger).to receive(:error).with(/authorizations_controller/) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + end + + it 'handles token exchange errors' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new) + .and_raise(StandardError, 'Invalid authorization code') + + post :create, params: { + account_id: account.id, + code: 'invalid_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('Invalid authorization code') + end + + it 'handles channel already exists error' do + allow(Whatsapp::EmbeddedSignupService).to receive(:new) + .and_raise(StandardError, 'Channel already exists') + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq('Channel already exists') + end + end + + context 'when user is not authorized for the account' do + let(:other_account) { create(:account) } + + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'returns unauthorized' do + post :create, params: { + account_id: other_account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when user is an administrator' do + let(:administrator) { create(:user, account: account, role: :administrator) } + + before do + sign_in(administrator) + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'allows channel creation' do + embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService) + whatsapp_channel = create(:channel_whatsapp, account: account) + inbox = create(:inbox, account: account, channel: whatsapp_channel) + + allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service) + allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel) + allow(whatsapp_channel).to receive(:inbox).and_return(inbox) + + # Stub webhook setup service + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + + post :create, params: { + account_id: account.id, + code: 'test_code', + business_id: 'test_business_id', + waba_id: 'test_waba_id' + } + + expect(response).to have_http_status(:success) + end + end + end + end +end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 4368807fe..34ef264b5 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -61,4 +61,85 @@ RSpec.describe Channel::Whatsapp do expect(channel.provider_config['webhook_verify_token']).to eq '123' end end + + describe 'webhook setup after creation' do + let(:account) { create(:account) } + let(:webhook_service) { instance_double(Whatsapp::WebhookSetupService) } + + before do + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + end + + context 'when channel is created through embedded signup' do + it 'sets up webhooks automatically' do + expect(Whatsapp::WebhookSetupService).to receive(:new).with( + anything, + 'test_waba_id', + 'test_access_token' + ) + expect(webhook_service).to receive(:perform) + + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'source' => 'embedded_signup', + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end + + it 'does not raise error if webhook setup fails' do + allow(webhook_service).to receive(:perform).and_raise(StandardError, 'Webhook error') + + expect do + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'source' => 'embedded_signup', + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end.not_to raise_error + end + end + + context 'when channel is created through manual setup' do + it 'does not setup webhooks' do + expect(Whatsapp::WebhookSetupService).not_to receive(:new) + + create(:channel_whatsapp, + account: account, + provider: 'whatsapp_cloud', + provider_config: { + 'business_account_id' => 'test_waba_id', + 'api_key' => 'test_access_token' + }, + validate_provider_config: false, + sync_templates: false) + end + end + + context 'when channel is created with different provider' do + it 'does not setup webhooks for 360dialog provider' do + expect(Whatsapp::WebhookSetupService).not_to receive(:new) + + create(:channel_whatsapp, + account: account, + provider: 'default', + provider_config: { + 'source' => 'embedded_signup', + 'api_key' => 'test_360dialog_key' + }, + validate_provider_config: false, + sync_templates: false) + end + end + end end diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb new file mode 100644 index 000000000..1cf0b87f2 --- /dev/null +++ b/spec/models/concerns/featurable_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe Featurable do + let(:account) { create(:account) } + + describe 'WhatsApp embedded signup feature' do + it 'is disabled by default' do + expect(account.feature_whatsapp_embedded_signup?).to be false + expect(account.feature_enabled?('whatsapp_embedded_signup')).to be false + end + + describe '#enable_features!' do + it 'enables the whatsapp embedded signup feature' do + account.enable_features!(:whatsapp_embedded_signup) + expect(account.feature_whatsapp_embedded_signup?).to be true + expect(account.feature_enabled?('whatsapp_embedded_signup')).to be true + end + + it 'enables multiple features at once' do + account.enable_features!(:whatsapp_embedded_signup, :help_center) + expect(account.feature_whatsapp_embedded_signup?).to be true + expect(account.feature_help_center?).to be true + end + end + + describe '#disable_features!' do + before do + account.enable_features!(:whatsapp_embedded_signup) + end + + it 'disables the whatsapp embedded signup feature' do + expect(account.feature_whatsapp_embedded_signup?).to be true + + account.disable_features!(:whatsapp_embedded_signup) + expect(account.feature_whatsapp_embedded_signup?).to be false + end + end + + describe '#enabled_features' do + it 'includes whatsapp_embedded_signup when enabled' do + account.enable_features!(:whatsapp_embedded_signup) + expect(account.enabled_features).to include('whatsapp_embedded_signup' => true) + end + + it 'does not include whatsapp_embedded_signup when disabled' do + account.disable_features!(:whatsapp_embedded_signup) + expect(account.enabled_features).not_to include('whatsapp_embedded_signup' => true) + end + end + + describe '#all_features' do + it 'includes whatsapp_embedded_signup in all features list' do + expect(account.all_features).to have_key('whatsapp_embedded_signup') + end + end + end +end diff --git a/spec/services/whatsapp/channel_creation_service_spec.rb b/spec/services/whatsapp/channel_creation_service_spec.rb index 80d1e378b..1c1f46232 100644 --- a/spec/services/whatsapp/channel_creation_service_spec.rb +++ b/spec/services/whatsapp/channel_creation_service_spec.rb @@ -19,6 +19,11 @@ describe Whatsapp::ChannelCreationService do # Clean up any existing channels to avoid phone number conflicts Channel::Whatsapp.destroy_all + # Stub the webhook setup service to prevent HTTP calls during tests + webhook_service = instance_double(Whatsapp::WebhookSetupService) + allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service) + allow(webhook_service).to receive(:perform) + # Stub the provider validation and sync_templates allow(Channel::Whatsapp).to receive(:new).and_wrap_original do |method, *args| channel = method.call(*args) @@ -111,4 +116,4 @@ describe Whatsapp::ChannelCreationService do end end end -end \ No newline at end of file +end diff --git a/spec/services/whatsapp/embedded_signup_service_spec.rb b/spec/services/whatsapp/embedded_signup_service_spec.rb index 3c420f497..c799f8c83 100644 --- a/spec/services/whatsapp/embedded_signup_service_spec.rb +++ b/spec/services/whatsapp/embedded_signup_service_spec.rb @@ -32,7 +32,6 @@ describe Whatsapp::EmbeddedSignupService do let(:phone_info_service) { instance_double(Whatsapp::PhoneInfoService) } let(:token_validation_service) { instance_double(Whatsapp::TokenValidationService) } let(:channel_creation_service) { instance_double(Whatsapp::ChannelCreationService) } - let(:webhook_setup_service) { instance_double(Whatsapp::WebhookSetupService) } before do allow(GlobalConfig).to receive(:clear_cache) @@ -53,9 +52,9 @@ describe Whatsapp::EmbeddedSignupService do .and_return(channel_creation_service) allow(channel_creation_service).to receive(:perform).and_return(channel) - allow(Whatsapp::WebhookSetupService).to receive(:new) - .with(channel, waba_id, access_token).and_return(webhook_setup_service) - allow(webhook_setup_service).to receive(:perform) + # Webhook setup is now handled in the channel after_create callback + # So we stub it at the channel level + allow(channel).to receive(:setup_webhooks) end it 'orchestrates all services in the correct order' do @@ -64,7 +63,6 @@ describe Whatsapp::EmbeddedSignupService do expect(phone_info_service).to receive(:perform).ordered expect(token_validation_service).to receive(:perform).ordered expect(channel_creation_service).to receive(:perform).ordered - expect(webhook_setup_service).to receive(:perform).ordered result = service.perform expect(result).to eq(channel) @@ -125,4 +123,4 @@ describe Whatsapp::EmbeddedSignupService do end end end -end \ No newline at end of file +end