From 8a760da6eb43ea5d81cdd22b021422a60e702a55 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 15 Jun 2026 17:57:44 +0530 Subject: [PATCH] refactor(billing): gate multi-currency on MULTIPLE_CURRENCY_SUPPORTED config instead of feature flag --- app/controllers/dashboard_controller.rb | 1 + .../dashboard/settings/billing/Index.vue | 10 +++---- app/javascript/shared/store/globalConfig.js | 2 ++ config/features.yml | 7 ++--- config/installation_config.yml | 6 ++++ ..._reply_flag_for_billing_currency_switch.rb | 20 ------------- db/schema.rb | 2 +- enterprise/app/models/enterprise/account.rb | 3 ++ .../services/enterprise/billing/currencies.rb | 6 ++++ .../billing/currency_switch_eligibility.rb | 2 +- spec/enterprise/models/account_spec.rb | 28 +++++++++++++++++++ .../billing/switch_currency_service_spec.rb | 6 ++-- 12 files changed, 59 insertions(+), 34 deletions(-) delete mode 100644 db/migrate/20260615000000_repurpose_quoted_email_reply_flag_for_billing_currency_switch.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index b6df015f7..5864ee529 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -24,6 +24,7 @@ class DashboardController < ActionController::Base DISABLE_USER_PROFILE_UPDATE DEPLOYMENT_ENV INSTALLATION_PRICING_PLAN + MULTIPLE_CURRENCY_SUPPORTED ].freeze before_action :set_application_pack diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue index d4480bbaf..4e7765f70 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue @@ -27,8 +27,7 @@ import ButtonV4 from 'next/button/Button.vue'; const router = useRouter(); const { t } = useI18n(); -const { currentAccount, isOnChatwootCloud, isCloudFeatureEnabled } = - useAccount(); +const { currentAccount, isOnChatwootCloud } = useAccount(); const { captainEnabled, captainLimits, @@ -39,6 +38,7 @@ const { } = useCaptain(); const uiFlags = useMapGetter('accounts/getUIFlags'); +const globalConfig = useMapGetter('globalConfig/get'); const store = useStore(); const BILLING_REFRESH_ATTEMPTED = 'billing_refresh_attempted'; @@ -138,9 +138,9 @@ const handleBillingPageLogic = async () => { const isSwitchingCurrency = computed(() => uiFlags.value.isSwitchingCurrency); -// Currency switching is rolled out per-account via the billing_currency_switch feature flag. -const showCurrencyToggle = computed(() => - isCloudFeatureEnabled('billing_currency_switch') +// Currency switching is shown only when multi-currency billing is enabled for the installation. +const showCurrencyToggle = computed( + () => globalConfig.value.multipleCurrencySupported ); const currentBillingCurrency = computed(() => diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index c1786238b..52c90c44b 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -25,6 +25,7 @@ const { DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate, DEPLOYMENT_ENV: deploymentEnv, ACTIVE_PLATFORM_BANNERS: activePlatformBanners, + MULTIPLE_CURRENCY_SUPPORTED: multipleCurrencySupported, } = window.globalConfig || {}; const state = { @@ -51,6 +52,7 @@ const state = { widgetBrandURL, isEnterprise: parseBoolean(isEnterprise), activePlatformBanners: activePlatformBanners || [], + multipleCurrencySupported: parseBoolean(multipleCurrencySupported), }; export const getters = { diff --git a/config/features.yml b/config/features.yml index cffa88588..03105588b 100644 --- a/config/features.yml +++ b/config/features.yml @@ -216,11 +216,10 @@ display_name: Reply Mailer Migration enabled: false chatwoot_internal: true -- name: billing_currency_switch - display_name: Billing Currency Switch +- name: quoted_email_reply + display_name: Quoted Email Reply enabled: false - premium: true - chatwoot_internal: true + deprecated: true - name: companies display_name: Companies enabled: false diff --git a/config/installation_config.yml b/config/installation_config.yml index 1440943da..93b2817ed 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -262,6 +262,12 @@ display_title: 'Planwise Features List' value: description: 'Config to features and their associated plans' +- name: MULTIPLE_CURRENCY_SUPPORTED + display_title: 'Multiple Currency Support' + value: false + description: 'Enable multi-currency billing: show the billing currency switch and onboard new pt_BR accounts in BRL' + locked: false + type: boolean - name: DEPLOYMENT_ENV value: self-hosted description: 'The deployment environment of the installation, to differentiate between Chatwoot cloud and self-hosted' diff --git a/db/migrate/20260615000000_repurpose_quoted_email_reply_flag_for_billing_currency_switch.rb b/db/migrate/20260615000000_repurpose_quoted_email_reply_flag_for_billing_currency_switch.rb deleted file mode 100644 index 083b44570..000000000 --- a/db/migrate/20260615000000_repurpose_quoted_email_reply_flag_for_billing_currency_switch.rb +++ /dev/null @@ -1,20 +0,0 @@ -class RepurposeQuotedEmailReplyFlagForBillingCurrencySwitch < ActiveRecord::Migration[7.1] - def up - # The quoted_email_reply flag (deprecated) has been renamed to billing_currency_switch. - # They share a bit position, so disable it on any accounts that had quoted_email_reply - # enabled — otherwise they would silently start with billing_currency_switch on. - Account.feature_billing_currency_switch.find_each(batch_size: 100) do |account| - account.disable_features(:billing_currency_switch) - account.save!(validate: false) - end - - # Remove the stale quoted_email_reply entry from ACCOUNT_LEVEL_FEATURE_DEFAULTS. - # ConfigLoader only adds new flags; it never removes renamed ones. - config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS') - return if config&.value.blank? - - config.value = config.value.reject { |feature| feature['name'] == 'quoted_email_reply' } - config.save! - GlobalConfig.clear_cache - end -end diff --git a/db/schema.rb b/db/schema.rb index 6b73ea713..2060bcfda 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_06_15_000000) do +ActiveRecord::Schema[7.1].define(version: 2026_06_11_184600) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" diff --git a/enterprise/app/models/enterprise/account.rb b/enterprise/app/models/enterprise/account.rb index 0ca194454..9535a5ecb 100644 --- a/enterprise/app/models/enterprise/account.rb +++ b/enterprise/app/models/enterprise/account.rb @@ -75,6 +75,9 @@ module Enterprise::Account # Existing Stripe customers stay on USD (webhook backfills); only new accounts infer from locale. return Enterprise::Billing::Currencies::DEFAULT if custom_attributes&.dig('stripe_customer_id').present? + # New accounts onboard in their locale's currency only while multi-currency billing is enabled. + return Enterprise::Billing::Currencies::DEFAULT unless Enterprise::Billing::Currencies.multi_currency_supported? + Enterprise::Billing::Currencies.for_locale(locale) end diff --git a/enterprise/app/services/enterprise/billing/currencies.rb b/enterprise/app/services/enterprise/billing/currencies.rb index e2c059421..90a96e6ab 100644 --- a/enterprise/app/services/enterprise/billing/currencies.rb +++ b/enterprise/app/services/enterprise/billing/currencies.rb @@ -45,6 +45,12 @@ module Enterprise::Billing::Currencies LOCALE_DEFAULTS.fetch(locale.to_s, DEFAULT) end + # Master switch for multi-currency billing (currency switch UI + non-default onboarding currency). + # Read raw from InstallationConfig so a super-admin toggle takes effect without cache staleness. + def multi_currency_supported? + ActiveModel::Type::Boolean.new.cast(InstallationConfig.find_by(name: 'MULTIPLE_CURRENCY_SUPPORTED')&.value) + end + def country_for(code) COUNTRY_BY_CURRENCY[to_supported(code)] end diff --git a/enterprise/app/services/enterprise/billing/currency_switch_eligibility.rb b/enterprise/app/services/enterprise/billing/currency_switch_eligibility.rb index 0c2bfe8b8..4a05e3738 100644 --- a/enterprise/app/services/enterprise/billing/currency_switch_eligibility.rb +++ b/enterprise/app/services/enterprise/billing/currency_switch_eligibility.rb @@ -20,7 +20,7 @@ class Enterprise::Billing::CurrencySwitchEligibility private def validate! - raise Error, I18n.t('errors.billing.currency_switch_unavailable') unless account.feature_enabled?('billing_currency_switch') + raise Error, I18n.t('errors.billing.currency_switch_unavailable') unless Enterprise::Billing::Currencies.multi_currency_supported? raise Error, I18n.t('errors.billing.unsupported_currency') unless Enterprise::Billing::Currencies.supported?(currency) raise Error, I18n.t('errors.billing.same_currency') if target_currency == account.billing_currency raise Error, I18n.t('errors.billing.stripe_customer_not_configured') if stripe_customer_id.blank? diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb index c69a83256..b409778b5 100644 --- a/spec/enterprise/models/account_spec.rb +++ b/spec/enterprise/models/account_spec.rb @@ -11,6 +11,34 @@ RSpec.describe Account, type: :model do it { is_expected.to have_many(:custom_roles).dependent(:destroy_async) } end + describe '#billing_currency' do + let(:account) { create(:account, locale: 'pt_BR') } + + it 'returns the stored currency when set' do + account.update!(custom_attributes: { 'billing_currency' => 'brl' }) + expect(account.billing_currency).to eq('brl') + end + + it 'keeps existing stripe customers on usd' do + account.update!(custom_attributes: { 'stripe_customer_id' => 'cus_123' }) + expect(account.billing_currency).to eq('usd') + end + + context 'when multi-currency billing is enabled' do + before { create(:installation_config, name: 'MULTIPLE_CURRENCY_SUPPORTED', value: true) } + + it 'onboards a new pt_BR account in brl' do + expect(account.billing_currency).to eq('brl') + end + end + + context 'when multi-currency billing is disabled' do + it 'onboards a new pt_BR account in usd' do + expect(account.billing_currency).to eq('usd') + end + end + end + describe 'sla_policies' do let!(:account) { create(:account) } let!(:sla_policy) { create(:sla_policy, account: account) } diff --git a/spec/enterprise/services/enterprise/billing/switch_currency_service_spec.rb b/spec/enterprise/services/enterprise/billing/switch_currency_service_spec.rb index d208b4c30..35a3e5142 100644 --- a/spec/enterprise/services/enterprise/billing/switch_currency_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/switch_currency_service_spec.rb @@ -33,7 +33,7 @@ describe Enterprise::Billing::SwitchCurrencyService do 'price_ids' => { 'usd' => ['price_business_usd'], 'brl' => ['price_business_brl'] } } ]) - account.enable_features!(:billing_currency_switch) + create(:installation_config, name: 'MULTIPLE_CURRENCY_SUPPORTED', value: true) account.update!(custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id, billing_currency: 'usd' }) allow(Stripe::Subscription).to receive(:list).and_return(Struct.new(:data).new([active_subscription])) @@ -84,8 +84,8 @@ describe Enterprise::Billing::SwitchCurrencyService do ) end - it 'raises when the feature is not enabled' do - account.disable_features!(:billing_currency_switch) + it 'raises when multi-currency billing is not enabled' do + InstallationConfig.find_by(name: 'MULTIPLE_CURRENCY_SUPPORTED').update!(value: false) expect { service.perform }.to raise_error do |error| expect(error.class.name).to eq('Enterprise::Billing::SwitchCurrencyService::Error')