refactor(billing): gate multi-currency on MULTIPLE_CURRENCY_SUPPORTED config instead of feature flag

This commit is contained in:
Tanmay Deep Sharma
2026-06-15 17:57:44 +05:30
parent e1d512cddd
commit 8a760da6eb
12 changed files with 59 additions and 34 deletions
+1
View File
@@ -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
@@ -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(() =>
@@ -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 = {
+3 -4
View File
@@ -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
+6
View File
@@ -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'
@@ -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
+1 -1
View File
@@ -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"
@@ -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
@@ -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
@@ -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?
+28
View File
@@ -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) }
@@ -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')