use stripe gem beta version

This commit is contained in:
Tanmay Deep Sharma
2025-11-06 13:38:47 +05:30
parent 79bb3d0ef3
commit d51148951f
10 changed files with 46 additions and 223 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
-1
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
@@ -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')
@@ -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