From e1d512cddd1ba972b85aafa68e5f24b736b95bd8 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 15 Jun 2026 17:42:05 +0530 Subject: [PATCH] fix(billing): fall back to legacy default source when no compatible payment method --- .../billing/default_payment_method_reconciler.rb | 14 +++++++------- .../billing/topup_checkout_service_spec.rb | 9 +++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/enterprise/app/services/enterprise/billing/default_payment_method_reconciler.rb b/enterprise/app/services/enterprise/billing/default_payment_method_reconciler.rb index e2bf4d487..a1c5358d6 100644 --- a/enterprise/app/services/enterprise/billing/default_payment_method_reconciler.rb +++ b/enterprise/app/services/enterprise/billing/default_payment_method_reconciler.rb @@ -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 diff --git a/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb b/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb index fa42ea5ad..409bd591c 100644 --- a/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/topup_checkout_service_spec.rb @@ -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')