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
@@ -59,10 +59,15 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
return render json: { error: I18n.t('errors.topup.credits_required') }, status: :unprocessable_entity if params[:credits].blank?
|
||||
|
||||
service = Enterprise::Billing::TopupCheckoutService.new(account: @account)
|
||||
redirect_url = service.create_checkout_session(credits: params[:credits].to_i)
|
||||
render json: { redirect_url: redirect_url }
|
||||
rescue Enterprise::Billing::TopupCheckoutService::Error => e
|
||||
Rails.logger.error("Topup checkout failed for account #{@account.id}: #{e.message}")
|
||||
result = service.create_checkout_session(credits: params[:credits].to_i)
|
||||
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
limits: @account.limits,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
rescue Enterprise::Billing::TopupCheckoutService::Error, Stripe::StripeError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
process_subscription_updated
|
||||
when 'customer.subscription.deleted'
|
||||
process_subscription_deleted
|
||||
when 'checkout.session.completed'
|
||||
process_checkout_session_completed
|
||||
else
|
||||
Rails.logger.debug { "Unhandled event type: #{event.type}" }
|
||||
end
|
||||
@@ -92,31 +90,6 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||
end
|
||||
|
||||
def process_checkout_session_completed
|
||||
session = @event.data.object
|
||||
metadata = session.metadata
|
||||
|
||||
# Only process topup checkout sessions
|
||||
return unless metadata.present? && metadata['topup'] == 'true'
|
||||
|
||||
topup_account = Account.find_by(id: metadata['account_id'])
|
||||
return if topup_account.blank?
|
||||
|
||||
credits = metadata['credits'].to_i
|
||||
amount_cents = metadata['amount_cents'].to_i
|
||||
currency = metadata['currency'] || 'usd'
|
||||
|
||||
Rails.logger.info("Processing topup for account #{topup_account.id}: #{credits} credits, #{amount_cents} cents")
|
||||
Enterprise::Billing::TopupFulfillmentService.new(account: topup_account).fulfill(
|
||||
credits: credits,
|
||||
amount_cents: amount_cents,
|
||||
currency: currency
|
||||
)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: topup_account).capture_exception
|
||||
raise
|
||||
end
|
||||
|
||||
def update_plan_features
|
||||
if default_plan?
|
||||
disable_all_premium_features
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class Enterprise::Billing::TopupCheckoutService
|
||||
include BillingHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
class Error < StandardError; end
|
||||
|
||||
@@ -15,8 +14,14 @@ class Enterprise::Billing::TopupCheckoutService
|
||||
|
||||
def create_checkout_session(credits:)
|
||||
topup_option = validate_and_find_topup_option(credits)
|
||||
session = create_stripe_session(topup_option, credits)
|
||||
session.url
|
||||
charge_customer(topup_option, credits)
|
||||
fulfill_credits(credits, topup_option)
|
||||
|
||||
{
|
||||
credits: credits,
|
||||
amount: topup_option[:amount],
|
||||
currency: topup_option[:currency]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
@@ -24,69 +29,60 @@ class Enterprise::Billing::TopupCheckoutService
|
||||
def validate_and_find_topup_option(credits)
|
||||
raise Error, I18n.t('errors.topup.invalid_credits') unless credits.to_i.positive?
|
||||
raise Error, I18n.t('errors.topup.plan_not_eligible') if default_plan?(account)
|
||||
raise Error, I18n.t('errors.topup.stripe_customer_not_configured') if stripe_customer_id.blank?
|
||||
|
||||
topup_option = find_topup_option(credits)
|
||||
raise Error, I18n.t('errors.topup.invalid_option') unless topup_option
|
||||
|
||||
# Validate payment method exists
|
||||
validate_payment_method!
|
||||
|
||||
topup_option
|
||||
end
|
||||
|
||||
def create_stripe_session(topup_option, credits)
|
||||
Stripe::Checkout::Session.create(
|
||||
def validate_payment_method!
|
||||
customer = Stripe::Customer.retrieve(stripe_customer_id)
|
||||
|
||||
return if customer.invoice_settings.default_payment_method.present? || customer.default_source.present?
|
||||
|
||||
# Auto-set first payment method as default if available
|
||||
payment_methods = Stripe::PaymentMethod.list(customer: stripe_customer_id, limit: 1)
|
||||
raise Error, I18n.t('errors.topup.no_payment_method') if payment_methods.data.empty?
|
||||
|
||||
Stripe::Customer.update(stripe_customer_id, invoice_settings: { default_payment_method: payment_methods.data.first.id })
|
||||
end
|
||||
|
||||
def charge_customer(topup_option, credits)
|
||||
amount_cents = (topup_option[:amount] * 100).to_i
|
||||
currency = topup_option[:currency]
|
||||
description = "AI Credits Topup: #{credits} credits"
|
||||
|
||||
invoice = Stripe::Invoice.create(
|
||||
customer: stripe_customer_id,
|
||||
mode: 'payment',
|
||||
line_items: [build_line_item(topup_option, credits)],
|
||||
success_url: success_url,
|
||||
cancel_url: cancel_url,
|
||||
metadata: session_metadata(credits, topup_option),
|
||||
payment_method_types: ['card'],
|
||||
# Show saved payment methods and allow saving new ones
|
||||
saved_payment_method_options: {
|
||||
payment_method_save: 'enabled',
|
||||
allow_redisplay_filters: %w[always limited]
|
||||
},
|
||||
# Create invoice for this payment so it appears in customer portal
|
||||
invoice_creation: build_invoice_creation_data(credits, topup_option)
|
||||
currency: currency,
|
||||
collection_method: 'charge_automatically',
|
||||
auto_advance: false,
|
||||
description: description
|
||||
)
|
||||
|
||||
Stripe::InvoiceItem.create(
|
||||
customer: stripe_customer_id,
|
||||
amount: amount_cents,
|
||||
currency: currency,
|
||||
invoice: invoice.id,
|
||||
description: description
|
||||
)
|
||||
|
||||
Stripe::Invoice.finalize_invoice(invoice.id, { auto_advance: false })
|
||||
Stripe::Invoice.pay(invoice.id)
|
||||
end
|
||||
|
||||
def build_invoice_creation_data(credits, topup_option)
|
||||
{
|
||||
enabled: true,
|
||||
invoice_data: {
|
||||
description: "AI Credits Topup: #{credits} credits",
|
||||
metadata: session_metadata(credits, topup_option)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def build_line_item(topup_option, credits)
|
||||
{
|
||||
price_data: {
|
||||
currency: topup_option[:currency],
|
||||
unit_amount: (topup_option[:amount] * 100).to_i,
|
||||
product_data: { name: "AI Credits Topup: #{credits} credits" }
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
end
|
||||
|
||||
def session_metadata(credits, topup_option)
|
||||
{
|
||||
account_id: account.id.to_s,
|
||||
credits: credits.to_s,
|
||||
amount_cents: (topup_option[:amount] * 100).to_s,
|
||||
currency: topup_option[:currency],
|
||||
topup: 'true'
|
||||
}
|
||||
end
|
||||
|
||||
def success_url
|
||||
app_account_billing_settings_url(account_id: account.id, topup: 'success')
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
app_account_billing_settings_url(account_id: account.id)
|
||||
def fulfill_credits(credits, topup_option)
|
||||
Enterprise::Billing::TopupFulfillmentService.new(account: account).fulfill(
|
||||
credits: credits,
|
||||
amount_cents: (topup_option[:amount] * 100).to_i,
|
||||
currency: topup_option[:currency]
|
||||
)
|
||||
end
|
||||
|
||||
def stripe_customer_id
|
||||
|
||||
@@ -6,6 +6,8 @@ class Enterprise::Billing::TopupFulfillmentService
|
||||
create_stripe_credit_grant(credits, amount_cents, currency)
|
||||
update_account_credits(credits)
|
||||
end
|
||||
|
||||
Rails.logger.info("Topup fulfilled for account #{account.id}: #{credits} credits, #{amount_cents} cents")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
Reference in New Issue
Block a user