feat: billing brl pix new users (#14617)

## Linear ticket
- https://linear.app/chatwoot/issue/CW-7253/billing-brl-pix-new-users

## Description

New accounts that sign up in Brazilian Portuguese are now billed in BRL
instead of USD. Their Stripe customer is created with a Brazil address
and Portuguese locale (so the Stripe portal offers Real prices and PIX),
and the AI credit top-up flow shows packages priced in the account's
billing currency. Currency support is config-driven, so adding another
currency later is a configuration change rather than a code change.


## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- https://www.loom.com/share/c8d3d08c1b844ed6b820438d4209491a

## Screenshot
<img width="904" height="440" alt="image"
src="https://github.com/user-attachments/assets/6f19fad8-e6af-46ea-b99f-b0265bb9eeec"
/>


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Tanmay Deep Sharma
2026-07-06 16:35:38 +05:30
committed by GitHub
parent 8818d276b9
commit 11deffdd5d
24 changed files with 562 additions and 90 deletions
@@ -256,6 +256,14 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
{ 'name' => 'Hacker', 'product_id' => ['prod_hacker'], 'price_ids' => ['price_hacker'] },
{ 'name' => 'Business', 'product_id' => ['prod_business'], 'price_ids' => ['price_business'] }
])
create(:installation_config, name: 'CAPTAIN_TOPUP_OPTIONS', value: {
'usd' => [
{ 'credits' => 1000, 'amount' => 20.0 },
{ 'credits' => 2500, 'amount' => 50.0 },
{ 'credits' => 6000, 'amount' => 100.0 },
{ 'credits' => 12_000, 'amount' => 200.0 }
]
})
end
it 'returns unauthorized for unauthenticated user' do
@@ -82,7 +82,8 @@ describe Enterprise::Billing::CreateStripeCustomerService do
subscribed_quantity: 2,
plan_name: 'A Plan Name',
subscription_status: 'active',
subscription_ends_on: subscription_ends_on
subscription_ends_on: subscription_ends_on,
billing_currency: 'usd'
}.with_indifferent_access
)
end
@@ -95,7 +96,9 @@ describe Enterprise::Billing::CreateStripeCustomerService do
create_stripe_customer_service.new(account: account).perform
expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email })
expect(Stripe::Customer).to have_received(:create).with(
{ name: account.name, email: admin1.email }
)
expect(Stripe::Subscription)
.to have_received(:create)
.with({ customer: customer.id, items: [{ price: 'price_hacker_random', quantity: 2 }] })
@@ -108,10 +111,27 @@ describe Enterprise::Billing::CreateStripeCustomerService do
subscribed_quantity: 2,
plan_name: 'A Plan Name',
subscription_status: 'active',
subscription_ends_on: subscription_ends_on
subscription_ends_on: subscription_ends_on,
billing_currency: 'usd'
}.with_indifferent_access
)
end
it 'sets the billing country override when the account currency requires it' do
with_modified_env ENABLE_MULTI_CURRENCY_BILLING: 'true' do
account.update!(custom_attributes: { billing_currency: 'brl' })
customer = double
allow(Stripe::Customer).to receive(:create).and_return(customer)
allow(customer).to receive(:id).and_return('cus_random_number')
allow(Stripe::Subscription).to receive(:create).and_return(created_subscription)
create_stripe_customer_service.new(account: account).perform
expect(Stripe::Customer).to have_received(:create).with(
{ name: account.name, email: admin1.email, address: { country: 'BR' }, preferred_locales: ['pt-BR'] }
)
end
end
end
describe 'when checking for existing subscriptions' do
@@ -0,0 +1,36 @@
require 'rails_helper'
describe Enterprise::Billing::Currencies do
describe 'Brazilian Real (brl)' do
it 'is a supported currency' do
expect(described_class.supported?('brl')).to be(true)
end
it 'recognizes brl regardless of casing or surrounding whitespace' do
expect(described_class.supported?(' BRL ')).to be(true)
expect(described_class.normalize(' BRL ')).to eq('brl')
end
it 'keeps brl when coercing to a supported code' do
expect(described_class.to_supported('BRL')).to eq('brl')
end
it 'defaults the pt_BR account locale to brl' do
expect(described_class.for_locale('pt_BR')).to eq('brl')
end
it 'maps brl to Brazil and the pt-BR checkout locale' do
expect(described_class.country_for('brl')).to eq('BR')
expect(described_class.preferred_locale_for('brl')).to eq('pt-BR')
end
it 'falls back to the usd default for unsupported input' do
expect(described_class.to_supported('eur')).to eq('usd')
end
it 'does not set a country override for usd customers' do
expect(described_class.country_for('usd')).to be_nil
expect(described_class.preferred_locale_for('usd')).to be_nil
end
end
end
@@ -15,6 +15,15 @@ describe Enterprise::Billing::TopupCheckoutService do
{ 'name' => 'Business', 'product_id' => ['prod_business'], 'price_ids' => ['price_business'] }
])
create(:installation_config, name: 'CAPTAIN_TOPUP_OPTIONS', value: {
'usd' => [
{ 'credits' => 1000, 'amount' => 20.0 },
{ 'credits' => 2500, 'amount' => 50.0 },
{ 'credits' => 6000, 'amount' => 100.0 },
{ 'credits' => 12_000, 'amount' => 200.0 }
]
})
account.update!(
custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id },
limits: { 'captain_responses' => 500 }