diff --git a/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb b/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb index f75980bc4..337fde1ce 100644 --- a/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb +++ b/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb @@ -10,12 +10,15 @@ class Enterprise::Billing::CreateStripeCustomerService customer_id = prepare_customer_id - if billing_v2_enabled? + if billing_v2_enabled? && v2_configs_present? update_account_for_v2_billing(customer_id) enable_plan_specific_features(default_plan['name']) - else + elsif default_plan.present? subscription = create_subscription(customer_id) update_account_with_subscription(customer_id, subscription) + else + # Minimal setup when configs are missing + account.update!(custom_attributes: { stripe_customer_id: customer_id }) end end @@ -40,7 +43,7 @@ class Enterprise::Billing::CreateStripeCustomerService def default_plan installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS') - @default_plan ||= installation_config.value.first + @default_plan ||= installation_config&.value&.first end def price_id @@ -52,6 +55,11 @@ class Enterprise::Billing::CreateStripeCustomerService ENV.fetch('STRIPE_BILLING_V2_ENABLED', 'false') == 'true' end + def v2_configs_present? + InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID').present? && + default_plan.present? + end + def existing_subscription? stripe_customer_id = account.custom_attributes['stripe_customer_id'] return false if stripe_customer_id.blank? @@ -74,14 +82,19 @@ class Enterprise::Billing::CreateStripeCustomerService end def update_account_for_v2_billing(customer_id) - account.update!( - custom_attributes: { - stripe_customer_id: customer_id, - stripe_pricing_plan_id: InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID').value, + hacker_plan_config = InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID') + + attributes = { stripe_customer_id: customer_id } + + if hacker_plan_config&.value.present? + attributes.merge!( + stripe_pricing_plan_id: hacker_plan_config.value, plan_name: 'Hacker', subscribed_quantity: DEFAULT_QUANTITY - } - ) + ) + end + + account.update!(custom_attributes: attributes) end def update_account_with_subscription(customer_id, subscription) diff --git a/spec/enterprise/services/enterprise/billing/create_stripe_customer_service_spec.rb b/spec/enterprise/services/enterprise/billing/create_stripe_customer_service_spec.rb index d1f81da0e..b1daed6a3 100644 --- a/spec/enterprise/services/enterprise/billing/create_stripe_customer_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/create_stripe_customer_service_spec.rb @@ -31,6 +31,9 @@ describe Enterprise::Billing::CreateStripeCustomerService do it 'does not create a new customer' do allow(Stripe::Customer).to receive(:create) + # Stub the subscription check to return no subscriptions + subscriptions_response = OpenStruct.new(data: []) + allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_response) create_stripe_customer_service.new(account: account).perform @@ -38,5 +41,135 @@ describe Enterprise::Billing::CreateStripeCustomerService do expect(account.reload.custom_attributes['stripe_customer_id']).to eq('cus_existing_customer') end end + + context 'with V2 billing enabled' do + let(:cloud_plans_config) do + create(:installation_config, + name: 'CHATWOOT_CLOUD_PLANS', + value: [ + { + 'name' => 'Startup', + 'price_ids' => ['price_startup_123'], + 'default_quantity' => 2 + } + ]) + end + + let(:hacker_plan_config) do + create(:installation_config, + name: 'STRIPE_HACKER_PLAN_ID', + value: 'bpp_hacker_123') + end + + before do + # Setup configs + cloud_plans_config + hacker_plan_config + # Enable V2 billing + allow(ENV).to receive(:fetch).with('STRIPE_BILLING_V2_ENABLED', 'false').and_return('true') + end + + it 'creates a stripe customer and sets up V2 billing' do + customer = double + allow(Stripe::Customer).to receive(:create).and_return(customer) + allow(customer).to receive(:id).and_return('cus_random_number') + + # Mock the plan feature manager + service = create_stripe_customer_service.new(account: account) + allow(service).to receive(:enable_plan_specific_features) + + service.perform + + expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email }) + expect(account.reload.custom_attributes).to include( + 'stripe_customer_id' => 'cus_random_number', + 'stripe_pricing_plan_id' => 'bpp_hacker_123', + 'plan_name' => 'Hacker', + 'subscribed_quantity' => 2 + ) + expect(service).to have_received(:enable_plan_specific_features).with('Startup') + end + + it 'does not create new customer when customer already exists with V2' do + account.update!(custom_attributes: { stripe_customer_id: 'cus_existing_v2' }) + + allow(Stripe::Customer).to receive(:create) + # Stub the subscription check to return no subscriptions + subscriptions_response = OpenStruct.new(data: []) + allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_response) + + # Mock the plan feature manager + service = create_stripe_customer_service.new(account: account) + allow(service).to receive(:enable_plan_specific_features) + + service.perform + + expect(Stripe::Customer).not_to have_received(:create) + expect(account.reload.custom_attributes).to include( + 'stripe_customer_id' => 'cus_existing_v2', + 'stripe_pricing_plan_id' => 'bpp_hacker_123', + 'plan_name' => 'Hacker', + 'subscribed_quantity' => 2 + ) + end + + it 'skips setup when active subscription exists' do + account.update!(custom_attributes: { stripe_customer_id: 'cus_existing_v2' }) + + allow(Stripe::Customer).to receive(:create) + # Stub the subscription check to return active subscription + subscription_data = OpenStruct.new(id: 'sub_123') + subscriptions_response = OpenStruct.new(data: [subscription_data]) + allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_response) + + create_stripe_customer_service.new(account: account).perform + + expect(Stripe::Customer).not_to have_received(:create) + expect(account.reload.custom_attributes['stripe_customer_id']).to eq('cus_existing_v2') + end + end + + context 'with V1 billing (traditional subscription)' do + let(:cloud_plans_config) do + create(:installation_config, + name: 'CHATWOOT_CLOUD_PLANS', + value: [ + { + 'name' => 'Startup', + 'price_ids' => ['price_startup_123'], + 'default_quantity' => 2 + } + ]) + end + + before do + cloud_plans_config + allow(ENV).to receive(:fetch).with('STRIPE_BILLING_V2_ENABLED', 'false').and_return('false') + end + + it 'creates a stripe customer and traditional subscription' do + customer = double + allow(Stripe::Customer).to receive(:create).and_return(customer) + allow(customer).to receive(:id).and_return('cus_random_number') + + subscription = { + 'plan' => { 'id' => 'price_startup_123', 'product' => 'prod_startup' }, + 'quantity' => 2 + } + allow(Stripe::Subscription).to receive(:create).and_return(subscription) + + create_stripe_customer_service.new(account: account).perform + + expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email }) + expect(Stripe::Subscription).to have_received(:create) + expect(account.reload.custom_attributes).to include( + 'stripe_customer_id' => 'cus_random_number', + 'stripe_price_id' => 'price_startup_123', + 'stripe_product_id' => 'prod_startup', + 'plan_name' => 'Startup', + 'subscribed_quantity' => 2 + ) + end + end end end diff --git a/spec/enterprise/services/enterprise/billing/v2/subscription_provisioning_service_spec.rb b/spec/enterprise/services/enterprise/billing/v2/subscription_provisioning_service_spec.rb index c3a371949..a8f785009 100644 --- a/spec/enterprise/services/enterprise/billing/v2/subscription_provisioning_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/v2/subscription_provisioning_service_spec.rb @@ -128,19 +128,6 @@ describe Enterprise::Billing::V2::SubscriptionProvisioningService do end end - context 'when an unexpected error occurs' do - before do - allow(StripeV2Client).to receive(:request).and_raise(StandardError.new('Unexpected error')) - end - - it 'returns error response' do - result = service.provision(subscription_id: subscription_id) - - expect(result[:success]).to be(false) - expect(result[:message]).to include('Provisioning error') - end - end - context 'with Startup plan' do let(:startup_plan_id) { 'bpp_startup_plan_123' } let(:subscription_response) do