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:
committed by
GitHub
parent
cc86b8c7f1
commit
eb759255d8
@@ -245,6 +245,78 @@ RSpec.describe 'Enterprise Billing APIs', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /enterprise/api/v1/accounts/{account.id}/topup_checkout' do
|
||||
let(:stripe_customer_id) { 'cus_test123' }
|
||||
let(:invoice_settings) { Struct.new(:default_payment_method).new('pm_test123') }
|
||||
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'] }
|
||||
])
|
||||
end
|
||||
|
||||
it 'returns unauthorized for unauthenticated user' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout", as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for agent' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { credits: 1000 },
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
context 'when it is an admin' do
|
||||
before do
|
||||
account.update!(
|
||||
custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id },
|
||||
limits: { 'captain_responses' => 1000 }
|
||||
)
|
||||
allow(Stripe::Customer).to receive(:retrieve).with(stripe_customer_id).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
|
||||
|
||||
it 'successfully processes topup and returns correct response' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { credits: 1000 },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['credits']).to eq(1000)
|
||||
expect(json_response['amount']).to eq(20.0)
|
||||
expect(json_response['limits']['captain_responses']).to eq(2000)
|
||||
end
|
||||
|
||||
it 'returns error when credits parameter is missing' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'returns error for invalid credits amount' do
|
||||
post "/enterprise/api/v1/accounts/#{account.id}/topup_checkout",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: { credits: 999 },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /enterprise/api/v1/accounts/{account.id}/toggle_deletion' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user