diff --git a/config/features.yml b/config/features.yml index 62bcab2da..39c8a53af 100644 --- a/config/features.yml +++ b/config/features.yml @@ -257,3 +257,7 @@ display_name: Data Import enabled: false column: feature_flags_ext_1 +- name: api_and_webhooks + display_name: API and Webhooks + enabled: true + column: feature_flags_ext_1 diff --git a/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb b/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb index 205bc348e..435b1f3d1 100644 --- a/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb +++ b/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb @@ -18,6 +18,7 @@ class Enterprise::Billing::ReconcilePlanFeaturesService advanced_search linear_integration channel_voice + api_and_webhooks ].freeze BUSINESS_PLAN_FEATURES = %w[ diff --git a/enterprise/app/services/internal/accounts/internal_attributes_service.rb b/enterprise/app/services/internal/accounts/internal_attributes_service.rb index 593cea799..00c3d3636 100644 --- a/enterprise/app/services/internal/accounts/internal_attributes_service.rb +++ b/enterprise/app/services/internal/accounts/internal_attributes_service.rb @@ -54,7 +54,7 @@ class Internal::Accounts::InternalAttributesService def valid_feature_list Enterprise::Billing::ReconcilePlanFeaturesService::BUSINESS_PLAN_FEATURES + Enterprise::Billing::ReconcilePlanFeaturesService::ENTERPRISE_PLAN_FEATURES + - %w[inbound_emails] + %w[inbound_emails api_and_webhooks] end # Account notes functionality removed for now diff --git a/lib/tasks/feature_defaults.rake b/lib/tasks/feature_defaults.rake new file mode 100644 index 000000000..6b6e0443a --- /dev/null +++ b/lib/tasks/feature_defaults.rake @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength +namespace :feature_defaults do + desc 'Interactively toggle a feature on/off in ACCOUNT_LEVEL_FEATURE_DEFAULTS (affects new account signups only)' + task toggle: :environment do + config = InstallationConfig.find_by!(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS') + + loop do + features = config.value + print_feature_list(features) + + print "\nEnter the number of the feature to toggle (or 'q' to quit): " + input = $stdin.gets.chomp + break if input.casecmp('q').zero? + + feature = select_feature(features, input) + if feature.nil? + puts 'Invalid selection.' + next + end + + toggle_feature(config, features, feature) + end + + puts 'Done.' + end + + def print_feature_list(features) + puts "\n#{'#'.ljust(4)}#{'name'.ljust(35)}#{'display_name'.ljust(30)}enabled" + features.each_with_index do |feature, index| + puts "#{(index + 1).to_s.ljust(4)}#{feature['name'].to_s.ljust(35)}#{feature['display_name'].to_s.ljust(30)}#{feature['enabled']}" + end + end + + def select_feature(features, input) + index = Integer(input, exception: false) + return nil if index.nil? || !index.between?(1, features.length) + + features[index - 1] + end + + def toggle_feature(config, features, feature) + print "#{feature['name']} is currently enabled: #{feature['enabled']}. Type 'true' or 'false' to set (anything else cancels): " + input = $stdin.gets.chomp + + case input + when 'true' + new_state = true + when 'false' + new_state = false + else + puts 'Cancelled.' + return + end + + feature['enabled'] = new_state + config.value = features + config.save! + GlobalConfig.clear_cache + puts "Updated #{feature['name']} to enabled: #{new_state}" + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/enterprise/services/enterprise/billing/reconcile_plan_features_service_spec.rb b/spec/enterprise/services/enterprise/billing/reconcile_plan_features_service_spec.rb new file mode 100644 index 000000000..64be87ff4 --- /dev/null +++ b/spec/enterprise/services/enterprise/billing/reconcile_plan_features_service_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Enterprise::Billing::ReconcilePlanFeaturesService do + let(:account) { create(:account) } + + before do + create(:installation_config, { + name: 'CHATWOOT_CLOUD_PLANS', + value: [ + { 'name' => 'Hacker', 'product_id' => ['plan_id_hacker'], 'price_ids' => ['price_hacker'] }, + { 'name' => 'Startups', 'product_id' => ['plan_id_startups'], 'price_ids' => ['price_startups'] } + ] + }) + end + + describe '#perform' do + context 'with api_and_webhooks feature' do + it 'enables the feature for a paid plan with an active subscription' do + account.update!(custom_attributes: { 'plan_name' => 'Startups', 'subscription_status' => 'active' }) + + described_class.new(account: account).perform + + expect(account.reload).to be_feature_enabled('api_and_webhooks') + end + + it 'enables the feature for a paid plan on trial' do + account.update!(custom_attributes: { 'plan_name' => 'Startups', 'subscription_status' => 'trialing' }) + + described_class.new(account: account).perform + + expect(account.reload).to be_feature_enabled('api_and_webhooks') + end + + it 'disables the feature on the default plan' do + account.enable_features!('api_and_webhooks') + account.update!(custom_attributes: { 'plan_name' => 'Hacker', 'subscription_status' => 'active' }) + + described_class.new(account: account).perform + + expect(account.reload).not_to be_feature_enabled('api_and_webhooks') + end + + it 'keeps the feature enabled when manually managed' do + account.update!(custom_attributes: { 'plan_name' => 'Hacker', 'subscription_status' => 'trialing' }) + Internal::Accounts::InternalAttributesService.new(account).manually_managed_features = ['api_and_webhooks'] + + described_class.new(account: account).perform + + expect(account.reload).to be_feature_enabled('api_and_webhooks') + end + end + end +end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index a4932d635..00b464f73 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -108,6 +108,8 @@ RSpec.describe Account do it 'configures the account feature flag extension column' do expect(described_class.flag_columns).to include('feature_flags', 'feature_flags_ext_1') + expect(described_class.flag_mapping['feature_flags_ext_1']).to eq(feature_whatsapp_manual_transfer: 1, feature_data_import: 1 << 1, + feature_api_and_webhooks: 1 << 2) expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_whatsapp_manual_transfer]).to eq(1) expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_data_import]).to eq(2) end