From d9348883edae96290096aeccd762c69769658216 Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Mon, 24 Nov 2025 19:09:00 +0530 Subject: [PATCH] add default payment method for topup and change of subscription --- .../billing/v2/invoice_payment_service.rb | 42 ++++++- .../billing/v2/topup_service_spec.rb | 111 +++++++++++++++++- 2 files changed, 145 insertions(+), 8 deletions(-) diff --git a/enterprise/app/services/enterprise/billing/v2/invoice_payment_service.rb b/enterprise/app/services/enterprise/billing/v2/invoice_payment_service.rb index 8b3ceaa8c..0cc6ca950 100644 --- a/enterprise/app/services/enterprise/billing/v2/invoice_payment_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/invoice_payment_service.rb @@ -9,6 +9,7 @@ class Enterprise::Billing::V2::InvoicePaymentService < Enterprise::Billing::V2:: # @return [Hash] { success:, invoice_id:, invoice_url:, amount:, status: } # Validate that customer has a default payment method + # If no default is set but payment methods exist, automatically set the first one as default # @return [Hash, nil] Returns error hash if validation fails, nil if success def validate_payment_method return { success: false, message: 'No Stripe customer ID found' } if stripe_customer_id.blank? @@ -16,10 +17,9 @@ class Enterprise::Billing::V2::InvoicePaymentService < Enterprise::Billing::V2:: customer = Stripe::Customer.retrieve(stripe_customer_id) if customer.invoice_settings.default_payment_method.nil? && customer.default_source.nil? - return { - success: false, - message: 'No default payment method found. Please add a default payment method before making a purchase.' - } + # No default payment method found - try to set one automatically + ensure_default_payment_method_result = ensure_default_payment_method(customer) + return ensure_default_payment_method_result unless ensure_default_payment_method_result.nil? end nil @@ -28,6 +28,40 @@ class Enterprise::Billing::V2::InvoicePaymentService < Enterprise::Billing::V2:: { success: false, message: "Error validating payment method: #{e.message}" } end + # Ensure a default payment method is set for the customer + # If payment methods exist but none is default, set the first one as default + # @param customer [Stripe::Customer] The Stripe customer object + # @return [Hash, nil] Returns error hash if no payment methods exist, nil if default is set successfully + def ensure_default_payment_method(customer) + payment_methods = fetch_customer_payment_methods(customer.id) + return no_payment_methods_error if payment_methods.data.empty? + + set_first_payment_method_as_default(customer.id, payment_methods.data.first) + nil + rescue Stripe::StripeError => e + Rails.logger.error("Failed to set default payment method: #{e.message}") + { success: false, message: "Error setting default payment method: #{e.message}" } + end + + def fetch_customer_payment_methods(customer_id) + Stripe::PaymentMethod.list(customer: customer_id, limit: 100) + end + + def no_payment_methods_error + { + success: false, + message: 'No payment methods found. Please add a payment method before making a purchase.' + } + end + + def set_first_payment_method_as_default(customer_id, payment_method) + Stripe::Customer.update( + customer_id, + invoice_settings: { default_payment_method: payment_method.id } + ) + Rails.logger.info("Automatically set payment method #{payment_method.id} as default for customer #{customer_id}") + end + # Create invoice with line items and charge immediately # # @param line_items [Array] Line items: [{ amount: (cents), description:, metadata: }] diff --git a/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb b/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb index c4d7cfda6..dfcd93c4b 100644 --- a/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/v2/topup_service_spec.rb @@ -25,11 +25,114 @@ describe Enterprise::Billing::V2::TopupService do end describe '#create_topup' do - it 'creates topup' do - result = service.create_topup(credits: 500) + context 'when customer has a default payment method' do + it 'creates topup successfully' do + result = service.create_topup(credits: 500) - expect(result[:success]).to be true - expect(result[:credits]).to eq(500) + expect(result[:success]).to be true + expect(result[:credits]).to eq(500) + expect(result[:amount]).to eq(50.0) + end + end + + context 'when customer has no default payment method but has payment methods available' do + let(:payment_methods_list) { OpenStruct.new(data: [OpenStruct.new(id: 'pm_first')]) } + + before do + allow(Stripe::Customer).to receive(:retrieve).and_return( + OpenStruct.new( + id: 'cus_123', + invoice_settings: OpenStruct.new(default_payment_method: nil), + default_source: nil + ) + ) + allow(Stripe::PaymentMethod).to receive(:list).and_return(payment_methods_list) + allow(Stripe::Customer).to receive(:update) + end + + it 'automatically sets default payment method and creates topup' do + result = service.create_topup(credits: 500) + + expect(Stripe::Customer).to have_received(:update).with( + 'cus_123', + invoice_settings: { default_payment_method: 'pm_first' } + ) + expect(result[:success]).to be true + expect(result[:credits]).to eq(500) + end + end + + context 'when customer has no payment methods at all' do + let(:empty_payment_methods) { OpenStruct.new(data: []) } + + before do + allow(Stripe::Customer).to receive(:retrieve).and_return( + OpenStruct.new( + id: 'cus_123', + invoice_settings: OpenStruct.new(default_payment_method: nil), + default_source: nil + ) + ) + allow(Stripe::PaymentMethod).to receive(:list).and_return(empty_payment_methods) + end + + it 'returns an error' do + result = service.create_topup(credits: 500) + + expect(result[:success]).to be false + expect(result[:message]).to include('No payment methods found') + end + end + + context 'when on Hacker plan' do + let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123', 'plan_name' => 'Hacker' }) } + + it 'returns an error' do + result = service.create_topup(credits: 500) + + expect(result[:success]).to be false + expect(result[:message]).to include('only available for Startup, Business, and Enterprise plans') + end + end + + context 'when no plan is set' do + let(:account) { create(:account, custom_attributes: { 'stripe_customer_id' => 'cus_123' }) } + + it 'returns an error' do + result = service.create_topup(credits: 500) + + expect(result[:success]).to be false + expect(result[:message]).to include('only available for Startup, Business, and Enterprise plans') + end + end + + context 'with invalid topup amount' do + it 'returns an error for zero credits' do + result = service.create_topup(credits: 0) + + expect(result[:success]).to be false + expect(result[:message]).to eq('Invalid topup amount') + end + + it 'returns an error for negative credits' do + result = service.create_topup(credits: -100) + + expect(result[:success]).to be false + expect(result[:message]).to eq('Invalid topup amount') + end + end + + context 'when topup amount is not supported' do + before do + allow(Enterprise::Billing::V2::TopupCatalog).to receive(:find_option).with(999).and_return(nil) + end + + it 'returns an error' do + result = service.create_topup(credits: 999) + + expect(result[:success]).to be false + expect(result[:message]).to eq('Unsupported topup amount') + end end end end