From d51148951fc5b20dbd30d105d8336d0a0d099a2a Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Thu, 6 Nov 2025 13:38:47 +0530 Subject: [PATCH] use stripe gem beta version --- Gemfile | 2 +- Gemfile.lock | 4 +- app/controllers/dashboard_controller.rb | 1 - .../concerns/stripe_v2_client_helper.rb | 74 ++++------------ .../billing/create_stripe_customer_service.rb | 58 ++++--------- .../enterprise/billing/v2/base_service.rb | 4 - .../billing/v2/checkout_session_service.rb | 38 +------- .../billing/v2/usage_reporter_service.rb | 1 - .../billing/credit_sync_job_spec.rb | 1 - .../create_stripe_customer_service_spec.rb | 86 ++----------------- 10 files changed, 46 insertions(+), 223 deletions(-) diff --git a/Gemfile b/Gemfile index abbd3332f..666ffea54 100644 --- a/Gemfile +++ b/Gemfile @@ -159,7 +159,7 @@ gem 'working_hours' gem 'pg_search' # Subscriptions, Billing -gem 'stripe' +gem 'stripe', '17.2.0.pre.beta.1' ## - helper gems --## ## to populate db with sample data diff --git a/Gemfile.lock b/Gemfile.lock index cf26b94a9..4a62aeab7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -902,7 +902,7 @@ GEM squasher (0.7.2) stackprof (0.2.25) statsd-ruby (1.5.0) - stripe (16.0.0) + stripe (17.2.0.pre.beta.1) telephone_number (1.4.20) test-prof (1.2.1) thor (1.4.0) @@ -1110,7 +1110,7 @@ DEPENDENCIES spring-watcher-listen squasher stackprof - stripe + stripe (= 17.2.0.pre.beta.1) telephone_number test-prof tidewave diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index f154cc315..d81b4c9da 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -71,7 +71,6 @@ class DashboardController < ActionController::Base WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), - STRIPE_BILLING_V2_ENABLED: GlobalConfigService.load('STRIPE_BILLING_V2_ENABLED', 'false'), GIT_SHA: GIT_HASH } end diff --git a/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb b/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb index bd04b0312..fe27cf2d3 100644 --- a/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb +++ b/enterprise/app/services/enterprise/billing/concerns/stripe_v2_client_helper.rb @@ -3,99 +3,57 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper private - # Generic Stripe V2 API request wrapper + # Stripe client instance with API key + def stripe_client + @stripe_client ||= Stripe::StripeClient.new(ENV.fetch('STRIPE_SECRET_KEY', nil)) + end + + # Generic Stripe V2 API request wrapper (for methods not available in gem) def stripe_v2_request(method, path, params = {}, api_version: nil) StripeV2Client.request(method, path, params, stripe_api_options(api_version)) end # Pricing Plan Subscriptions def retrieve_pricing_plan_subscription(subscription_id) - stripe_v2_request(:get, "/v2/billing/pricing_plan_subscriptions/#{subscription_id}") + stripe_client.v2.billing.pricing_plan_subscriptions.retrieve(subscription_id) end # Pricing Plans def retrieve_pricing_plan(pricing_plan_id) - stripe_v2_request(:get, "/v2/billing/pricing_plans/#{pricing_plan_id}") - end - - def create_pricing_plan(params) - stripe_v2_request(:post, '/v2/billing/pricing_plans', params) + stripe_client.v2.billing.pricing_plans.retrieve(pricing_plan_id) end + # gem not available - using custom HTTP client def update_pricing_plan(plan_id, params) stripe_v2_request(:post, "/v2/billing/pricing_plans/#{plan_id}", params) end # Billing Cadences def retrieve_billing_cadence(cadence_id) - stripe_v2_request(:get, "/v2/billing/cadences/#{cadence_id}") + stripe_client.v2.billing.cadences.retrieve(cadence_id) end - # Billing Intents + # gem not available - using custom HTTP client def create_billing_intent(params) stripe_v2_request(:post, '/v2/billing/intents', params) end def reserve_billing_intent(billing_intent) - stripe_v2_request(:post, "/v2/billing/intents/#{billing_intent.id}/reserve") + stripe_client.v2.billing.intents.reserve(billing_intent.id) end def commit_billing_intent(billing_intent) - stripe_v2_request(:post, "/v2/billing/intents/#{billing_intent.id}/commit") - end - - # Custom Pricing Units - def create_custom_pricing_unit(params) - stripe_v2_request(:post, '/v2/billing/custom_pricing_units', params) - end - - # Licensed Items - def create_licensed_item(params) - stripe_v2_request(:post, '/v2/billing/licensed_items', params) - end - - # License Fees - def create_license_fee(params) - stripe_v2_request(:post, '/v2/billing/license_fees', params) - end - - # Service Actions - def create_service_action(params) - stripe_v2_request(:post, '/v2/billing/service_actions', params) - end - - # Rate Cards - def create_rate_card(params) - stripe_v2_request(:post, '/v2/billing/rate_cards', params) - end - - def add_rate_to_card(card_id, params) - stripe_v2_request(:post, "/v2/billing/rate_cards/#{card_id}/rates", params) - end - - # Metered Items - def create_metered_item(params) - stripe_v2_request(:post, '/v2/billing/metered_items', params) - end - - # Meters (V1 API but used with V2) - def create_meter(params) - stripe_v2_request(:post, '/v1/billing/meters', params) - end - - # Pricing Plan Components - def add_pricing_plan_component(plan_id, params) - stripe_v2_request(:post, "/v2/billing/pricing_plans/#{plan_id}/components", params) + stripe_client.v2.billing.intents.commit(billing_intent.id) end # Checkout Sessions (V1 API but used with V2 plans) - def create_checkout_session(params, api_version: nil) - stripe_v2_request(:post, '/v1/checkout/sessions', params, api_version: api_version) + def create_checkout_session(params) + Stripe::Checkout::Session.create(params, { stripe_version: checkout_stripe_version }) end # Credit Grants (V1 API but used with V2) def retrieve_credit_grant(grant_id) - stripe_v2_request(:get, "/v1/billing/credit_grants/#{grant_id}") + Stripe::Billing::CreditGrant.retrieve(grant_id) end # API Options with support for custom versions 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 337fde1ce..15be677d2 100644 --- a/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb +++ b/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb @@ -8,26 +8,23 @@ class Enterprise::Billing::CreateStripeCustomerService def perform return if existing_subscription? - customer_id = prepare_customer_id + raise_config_error unless v2_configs_present? - if billing_v2_enabled? && v2_configs_present? - update_account_for_v2_billing(customer_id) - enable_plan_specific_features(default_plan['name']) - 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 + customer_id = prepare_customer_id + update_account_for_v2_billing(customer_id) + enable_plan_specific_features(default_plan['name']) end private def prepare_customer_id customer_id = account.custom_attributes['stripe_customer_id'] + test_clock = Stripe::TestHelpers::TestClock.create({ + frozen_time: Time.now.to_i, + name: 'Test Clock for Account' + }) if customer_id.blank? - customer = Stripe::Customer.create({ name: account.name, email: billing_email }) + customer = Stripe::Customer.create({ name: account.name, email: billing_email, test_clock: test_clock.id }) customer_id = customer.id end customer_id @@ -46,20 +43,15 @@ class Enterprise::Billing::CreateStripeCustomerService @default_plan ||= installation_config&.value&.first end - def price_id - price_ids = default_plan['price_ids'] - price_ids.first - end - - def billing_v2_enabled? - 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 raise_config_error + raise StandardError, 'V2 billing configuration is required. Please configure STRIPE_HACKER_PLAN_ID and CHATWOOT_CLOUD_PLANS.' + end + def existing_subscription? stripe_customer_id = account.custom_attributes['stripe_customer_id'] return false if stripe_customer_id.blank? @@ -74,17 +66,13 @@ class Enterprise::Billing::CreateStripeCustomerService subscriptions.data.present? end - def create_subscription(customer_id) - Stripe::Subscription.create( - customer: customer_id, - items: [{ price: price_id, quantity: default_quantity }] - ) - end - def update_account_for_v2_billing(customer_id) hacker_plan_config = InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID') - attributes = { stripe_customer_id: customer_id } + attributes = { + stripe_customer_id: customer_id, + stripe_billing_version: 'v2' + } if hacker_plan_config&.value.present? attributes.merge!( @@ -96,16 +84,4 @@ class Enterprise::Billing::CreateStripeCustomerService account.update!(custom_attributes: attributes) end - - def update_account_with_subscription(customer_id, subscription) - account.update!( - custom_attributes: { - stripe_customer_id: customer_id, - stripe_price_id: subscription['plan']['id'], - stripe_product_id: subscription['plan']['product'], - plan_name: default_plan['name'], - subscribed_quantity: subscription['quantity'] - } - ) - end end diff --git a/enterprise/app/services/enterprise/billing/v2/base_service.rb b/enterprise/app/services/enterprise/billing/v2/base_service.rb index e911cf0d8..7ab1ccd9c 100644 --- a/enterprise/app/services/enterprise/billing/v2/base_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/base_service.rb @@ -13,10 +13,6 @@ class Enterprise::Billing::V2::BaseService ) end - def v2_enabled? - ENV.fetch('STRIPE_BILLING_V2_ENABLED', 'false') == 'true' - end - def response_monthly_credits account.limits&.[]('captain_responses_monthly').to_i end diff --git a/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb b/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb index 3f0994598..502545f92 100644 --- a/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb @@ -1,21 +1,7 @@ -# V2 Billing Checkout Service -# -# This service creates Checkout Sessions for V2 Billing subscriptions using -# checkout_items with pricing_plan_subscription_item. -# class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2::BaseService include Enterprise::Billing::Concerns::PlanFeatureManager include Enterprise::Billing::Concerns::StripeV2ClientHelper - # Create a subscription checkout session - # - # Creates a Checkout Session with checkout_items containing the V2 Pricing Plan - # and component configurations for the license fee. - # - # @param pricing_plan_id [String] V2 Pricing Plan ID - # @param quantity [Integer] Number of licenses/seats - # @return [Hash] { success:, session_id:, redirect_url: } or error - # def create_subscription_checkout(pricing_plan_id:, quantity: 1) @pricing_plan_id = pricing_plan_id @quantity = quantity.to_i.positive? ? quantity.to_i : 1 @@ -26,9 +12,8 @@ class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2: validate_params store_pending_subscription_quantity - create_checkout_session - rescue Stripe::StripeError => e - { success: false, message: "Stripe API error: #{e.message}", error: e } + session = create_checkout_session + { success: true, redirect_url: session.url } end private @@ -46,16 +31,6 @@ class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2: }) end - # Create Checkout Session with checkout_items - # - # Uses V2 Pricing Plans directly via checkout_items parameter. - # This creates a subscription with V2 Billing features automatically. - # - def create_checkout_session - session = super(checkout_session_params, api_version: checkout_stripe_version) - build_success_response(session) - end - def checkout_session_params { customer: stripe_customer_id, @@ -107,13 +82,4 @@ class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2: billing_version: 'v2' } end - - def build_success_response(session) - session_url = session.respond_to?(:url) ? session.url : session['url'] - - { - success: true, - redirect_url: session_url - } - end end diff --git a/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb b/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb index 35a0c8434..989acc9dd 100644 --- a/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb @@ -1,6 +1,5 @@ class Enterprise::Billing::V2::UsageReporterService < Enterprise::Billing::V2::BaseService def report(credits_used, _feature = nil) - return { success: false, message: 'V2 billing not enabled' } unless v2_enabled? return { success: false, message: 'Missing Stripe configuration' } unless valid_configuration? event = Stripe::Billing::MeterEvent.create( diff --git a/spec/enterprise/jobs/enterprise/billing/credit_sync_job_spec.rb b/spec/enterprise/jobs/enterprise/billing/credit_sync_job_spec.rb index bb744bf79..d8c24fc97 100644 --- a/spec/enterprise/jobs/enterprise/billing/credit_sync_job_spec.rb +++ b/spec/enterprise/jobs/enterprise/billing/credit_sync_job_spec.rb @@ -5,7 +5,6 @@ RSpec.describe Enterprise::Billing::CreditSyncJob, type: :job do before do allow(ENV).to receive(:fetch).and_call_original - allow(ENV).to receive(:fetch).with('STRIPE_BILLING_V2_ENABLED', 'false').and_return('true') allow(ENV).to receive(:fetch).with('STRIPE_SECRET_KEY', nil).and_return('sk_test_123') allow(InstallationConfig).to receive(:find_by).and_call_original allow(InstallationConfig).to receive(:find_by).with(name: 'STRIPE_METER_ID') 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 b1daed6a3..a2fac700f 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 @@ -7,42 +7,15 @@ describe Enterprise::Billing::CreateStripeCustomerService do let!(:admin1) { create(:user, account: account, role: :administrator) } describe '#perform' do - context 'when customer does not exist' do - it 'creates a stripe customer and saves the customer_id' do - customer = double - allow(Stripe::Customer).to receive(:create).and_return(customer) - allow(customer).to receive(:id).and_return('cus_random_number') - - create_stripe_customer_service.new(account: account).perform - - expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email }) - expect(account.reload.custom_attributes).to eq( - { - stripe_customer_id: 'cus_random_number' - }.with_indifferent_access - ) + context 'when V2 configs are missing' do + it 'raises a configuration error' do + expect do + create_stripe_customer_service.new(account: account).perform + end.to raise_error(StandardError, /V2 billing configuration is required/) end end - context 'when customer already exists' do - before do - account.update!(custom_attributes: { stripe_customer_id: 'cus_existing_customer' }) - end - - 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 - - expect(Stripe::Customer).not_to have_received(:create) - expect(account.reload.custom_attributes['stripe_customer_id']).to eq('cus_existing_customer') - end - end - - context 'with V2 billing enabled' do + context 'with V2 billing' do let(:cloud_plans_config) do create(:installation_config, name: 'CHATWOOT_CLOUD_PLANS', @@ -65,8 +38,6 @@ describe Enterprise::Billing::CreateStripeCustomerService 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 @@ -83,6 +54,7 @@ describe Enterprise::Billing::CreateStripeCustomerService do 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_billing_version' => 'v2', 'stripe_pricing_plan_id' => 'bpp_hacker_123', 'plan_name' => 'Hacker', 'subscribed_quantity' => 2 @@ -107,6 +79,7 @@ describe Enterprise::Billing::CreateStripeCustomerService do expect(Stripe::Customer).not_to have_received(:create) expect(account.reload.custom_attributes).to include( 'stripe_customer_id' => 'cus_existing_v2', + 'stripe_billing_version' => 'v2', 'stripe_pricing_plan_id' => 'bpp_hacker_123', 'plan_name' => 'Hacker', 'subscribed_quantity' => 2 @@ -128,48 +101,5 @@ describe Enterprise::Billing::CreateStripeCustomerService do 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