fix(billing): fall back to legacy default source when no compatible payment method

This commit is contained in:
Tanmay Deep Sharma
2026-06-15 17:42:05 +05:30
parent bdb3bec11a
commit e1d512cddd
2 changed files with 16 additions and 7 deletions
@@ -1,28 +1,28 @@
# Ensures the customer's default Stripe payment method can actually bill the given currency. PIX/boleto
# are BRL-only, so when an account moves to a currency they can't pay we drop them as the default and
# pick a compatible method (a card) if one is attached. Incompatible methods stay attached for later.
# Keyed off invoice_settings.default_payment_method, since that's what Stripe Billing charges.
# Keyed off invoice_settings.default_payment_method (what Stripe Billing charges), inspecting it before
# falling back to a legacy default_source card so an incompatible invoice default can't be masked.
class Enterprise::Billing::DefaultPaymentMethodReconciler
pattr_initialize [:account!, :currency!]
# Returns the id of a currency-compatible default payment method, or nil if none is available.
def reconcile
current_default = current_default_payment_method
customer = Stripe::Customer.retrieve(stripe_customer_id)
current_default = customer.invoice_settings.default_payment_method
return current_default if compatible?(payment_methods.find { |method| method.id == current_default })
compatible = payment_methods.find { |method| compatible?(method) }
return make_default(compatible.id) if compatible
# No compatible attached PaymentMethod. Drop an incompatible invoice default so Stripe won't charge
# it, then fall back to a legacy default_source card (currency-agnostic) the invoice can still use.
unset_default if current_default.present?
nil
customer.default_source.presence
end
private
def current_default_payment_method
Stripe::Customer.retrieve(stripe_customer_id).invoice_settings.default_payment_method
end
def payment_methods
@payment_methods ||= Stripe::PaymentMethod.list(customer: stripe_customer_id, limit: 100).data
end
@@ -54,6 +54,15 @@ describe Enterprise::Billing::TopupCheckoutService do
expect(account.reload.limits['captain_responses']).to eq(1500)
end
it 'charges via a legacy default source when there is no invoice default or payment method' do
allow(Stripe::Customer).to receive(:retrieve).and_return(
Struct.new(:invoice_settings, :default_source).new(Struct.new(:default_payment_method).new(nil), 'card_legacy')
)
allow(Stripe::PaymentMethod).to receive(:list).and_return(Struct.new(:data).new([]))
expect(service.create_checkout_session(credits: 1000)[:credits]).to eq(1000)
end
it 'raises error for invalid credits' do
expect { service.create_checkout_session(credits: 500) }.to raise_error do |error|
expect(error.class.name).to eq('Enterprise::Billing::TopupCheckoutService::Error')