perf: update the logic to purchase credits (#12998)

## Description

- Replaces Stripe Checkout session flow with direct card charging for AI
credit top-ups
- Adds a two-step confirmation modal (select package → confirm purchase)
for better UX
- Creates Stripe invoice directly and charges the customer's default
payment method immediately

## Type of change

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

## How Has This Been Tested?

- Using the specs
- UI manual test cases

<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/52bdad46-cd0e-4927-b13f-54c6b6353bcc"
/>

<img width="945" height="580" alt="image"
src="https://github.com/user-attachments/assets/231bc7e9-41ac-440d-a93d-cba45a4d3e3e"
/>


## 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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
Tanmay Deep Sharma
2025-12-08 10:52:17 +05:30
committed by GitHub
parent cc86b8c7f1
commit eb759255d8
13 changed files with 414 additions and 135 deletions

View File

@@ -0,0 +1,60 @@
require 'rails_helper'
describe Enterprise::Billing::TopupCheckoutService do
subject(:service) { described_class.new(account: account) }
let(:account) { create(:account) }
let(:stripe_customer_id) { 'cus_test123' }
let(:invoice_settings) { Struct.new(:default_payment_method).new('pm_test') }
let(:stripe_customer) { Struct.new(:invoice_settings, :default_source).new(invoice_settings, nil) }
let(:stripe_invoice) { Struct.new(:id).new('inv_test123') }
before do
create(:installation_config, name: 'CHATWOOT_CLOUD_PLANS', value: [
{ 'name' => 'Hacker', 'product_id' => ['prod_hacker'], 'price_ids' => ['price_hacker'] },
{ 'name' => 'Business', 'product_id' => ['prod_business'], 'price_ids' => ['price_business'] }
])
account.update!(
custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id },
limits: { 'captain_responses' => 500 }
)
allow(Stripe::Customer).to receive(:retrieve).and_return(stripe_customer)
allow(Stripe::Invoice).to receive(:create).and_return(stripe_invoice)
allow(Stripe::InvoiceItem).to receive(:create)
allow(Stripe::Invoice).to receive(:finalize_invoice)
allow(Stripe::Invoice).to receive(:pay)
allow(Stripe::Billing::CreditGrant).to receive(:create)
end
describe '#create_checkout_session' do
it 'successfully processes topup and returns correct response' do
result = service.create_checkout_session(credits: 1000)
expect(result[:credits]).to eq(1000)
expect(result[:amount]).to eq(20.0)
expect(result[:currency]).to eq('usd')
end
it 'updates account limits after successful topup' do
service.create_checkout_session(credits: 1000)
expect(account.reload.limits['captain_responses']).to eq(1500)
end
it 'raises error for invalid credits' do
expect do
service.create_checkout_session(credits: 500)
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
end
it 'raises error when account is on free plan' do
account.update!(custom_attributes: { plan_name: 'Hacker', stripe_customer_id: stripe_customer_id })
expect do
service.create_checkout_session(credits: 1000)
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
end
end
end

View File

@@ -0,0 +1,36 @@
require 'rails_helper'
describe Enterprise::Billing::TopupFulfillmentService do
subject(:service) { described_class.new(account: account) }
let(:account) { create(:account) }
let(:stripe_customer_id) { 'cus_test123' }
before do
account.update!(
custom_attributes: { stripe_customer_id: stripe_customer_id },
limits: { 'captain_responses' => 1000 }
)
allow(Stripe::Billing::CreditGrant).to receive(:create)
end
describe '#fulfill' do
it 'adds credits to account limits' do
service.fulfill(credits: 1000, amount_cents: 2000, currency: 'usd')
expect(account.reload.limits['captain_responses']).to eq(2000)
end
it 'creates a Stripe credit grant' do
service.fulfill(credits: 1000, amount_cents: 2000, currency: 'usd')
expect(Stripe::Billing::CreditGrant).to have_received(:create).with(
hash_including(
customer: stripe_customer_id,
name: 'Topup: 1000 credits',
category: 'paid'
)
)
end
end
end