Merge remote-tracking branch 'origin/feature/stripe_v2' into feature/stripe_v2_fe

This commit is contained in:
Tanmay Deep Sharma
2025-11-07 03:39:08 +05:30
6 changed files with 47 additions and 148 deletions
@@ -15,7 +15,7 @@ class Enterprise::Billing::CreditSyncJob < ApplicationJob
Rails.logger.info '[CreditSyncJob] Starting credit sync for all accounts'
accounts_with_stripe = Account.where(
"custom_attributes->>'stripe_customer_id' IS NOT NULL AND custom_attributes->>'stripe_billing_version' = 2"
"custom_attributes->>'stripe_customer_id' IS NOT NULL AND (custom_attributes->>'stripe_billing_version')::integer = 2"
)
synced_count = 0
failed_count = 0
@@ -6,9 +6,9 @@ module Enterprise::Billing::Concerns::BillingIntentWorkflow
# Execute a billing intent with automatic reserve and commit
def execute_billing_intent(intent_params)
intent = create_billing_intent(intent_params)
reserve_billing_intent(intent)
reserve_billing_intent(intent['id'])
yield(intent) if block_given?
commit_billing_intent(intent)
commit_billing_intent(intent['id'])
intent
end
end
@@ -8,11 +8,6 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper
@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_client.v2.billing.pricing_plan_subscriptions.retrieve(subscription_id)
@@ -23,27 +18,27 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper
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_client.v2.billing.cadences.retrieve(cadence_id)
end
# gem not available - using custom HTTP client
def create_billing_intent(params)
stripe_v2_request(:post, '/v2/billing/intents', params)
response = Faraday.post('https://api.stripe.com/v2/billing/intents') do |req|
req.headers['Authorization'] = "Bearer #{ENV.fetch('STRIPE_SECRET_KEY', nil)}"
req.headers['Stripe-Version'] = default_stripe_version
req.headers['Content-Type'] = 'application/json'
req.body = params.to_json
end
JSON.parse(response.body)
end
def reserve_billing_intent(billing_intent)
stripe_client.v2.billing.intents.reserve(billing_intent.id)
def reserve_billing_intent(billing_intent_id)
stripe_client.v2.billing.intents.reserve(billing_intent_id)
end
def commit_billing_intent(billing_intent)
stripe_client.v2.billing.intents.commit(billing_intent.id)
def commit_billing_intent(billing_intent_id)
stripe_client.v2.billing.intents.commit(billing_intent_id)
end
# Checkout Sessions (V1 API but used with V2 plans)
@@ -56,14 +51,6 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper
Stripe::Billing::CreditGrant.retrieve(grant_id)
end
# API Options with support for custom versions
def stripe_api_options(custom_version = nil)
{
api_key: ENV.fetch('STRIPE_SECRET_KEY', nil),
stripe_version: custom_version || default_stripe_version
}
end
def default_stripe_version
'2025-10-29.preview'
end
-111
View File
@@ -1,111 +0,0 @@
module StripeV2Client
class << self
def request(method, path, params = {}, options = {})
api_key = options[:api_key] || ENV.fetch('STRIPE_SECRET_KEY', nil)
stripe_version = options[:stripe_version] || '2025-08-27.preview'
uri = URI("https://api.stripe.com#{path}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = build_request(method, uri, params, path)
request['Authorization'] = "Bearer #{api_key}"
request['Stripe-Version'] = stripe_version
response = http.request(request)
parse_response(response)
end
private
def build_request(method, uri, params, path)
case method
when :get
Net::HTTP::Get.new(uri)
when :post
req = Net::HTTP::Post.new(uri)
if v2_endpoint?(path)
# V2 endpoints use JSON
req.body = params.to_json unless params.empty?
req['Content-Type'] = 'application/json'
else
# V1 endpoints (including checkout sessions) use form data
# even with preview API versions
req.body = encode_nested_params(params) unless params.empty?
req['Content-Type'] = 'application/x-www-form-urlencoded'
end
req
when :delete
Net::HTTP::Delete.new(uri)
end
end
def v2_endpoint?(path)
path.start_with?('/v2/')
end
# Encode nested parameters for form submission
# Stripe expects nested params like: checkout_items[0][type]=value
def encode_nested_params(params, prefix = nil)
pairs = []
params.each do |key, value|
full_key = prefix ? "#{prefix}[#{key}]" : key.to_s
pairs.concat(encode_param_value(full_key, value))
end
pairs.join('&')
end
def encode_param_value(key, value)
case value
when Hash
[encode_nested_params(value, key)]
when Array
value.each_with_index.map { |item, index| encode_nested_params(item, "#{key}[#{index}]") }
when true, false
["#{CGI.escape(key)}=#{value}"]
when nil
[]
else
["#{CGI.escape(key)}=#{CGI.escape(value.to_s)}"]
end
end
def parse_response(response)
body = JSON.parse(response.body)
# Check for Stripe error responses
if body.is_a?(Hash) && body['error']
error = body['error']
raise Stripe::StripeError, "#{error['code']}: #{error['message']}"
end
# Convert to OpenStruct for dot notation access (mimicking Stripe SDK objects)
case body
when Hash
recursive_to_struct(body)
when Array
body.map { |item| recursive_to_struct(item) }
else
body
end
rescue JSON::ParserError
response.body
end
def recursive_to_struct(hash)
return hash unless hash.is_a?(Hash)
OpenStruct.new(hash.transform_values do |value|
case value
when Hash
recursive_to_struct(value)
when Array
value.map { |item| item.is_a?(Hash) ? recursive_to_struct(item) : item }
else
value
end
end)
end
end
end
@@ -59,7 +59,7 @@ describe Enterprise::Billing::CreateStripeCustomerService do
'plan_name' => 'Hacker',
'subscribed_quantity' => 2
)
expect(service).to have_received(:enable_plan_specific_features).with('Startup')
expect(service).to have_received(:enable_plan_specific_features).with('Hacker')
end
it 'does not create new customer when customer already exists with V2' do
@@ -16,24 +16,47 @@ describe Enterprise::Billing::V2::WebhookHandlerService do
end
let(:cadence_response) { OpenStruct.new(payer: OpenStruct.new(customer: 'cus_123')) }
let(:event) do
double('Stripe::Event', type: 'v2.billing.pricing_plan_subscription.servicing_activated', related_object: OpenStruct.new(id: 'bpps_sub_123')) # rubocop:disable RSpec/VerifiedDoubles
event_double = double
allow(event_double).to receive(:blank?).and_return(false)
allow(event_double).to receive(:type).and_return('v2.billing.pricing_plan_subscription.servicing_activated')
allow(event_double).to receive(:related_object).and_return(OpenStruct.new(id: 'bpps_sub_123'))
event_double
end
before do
account # Ensure account is created
create(:installation_config, name: 'STRIPE_BUSINESS_PLAN_ID', value: pricing_plan_id)
# Mock the Stripe API calls
allow(StripeV2Client).to receive(:request)
.with(:get, '/v2/billing/pricing_plan_subscriptions/bpps_sub_123', anything, anything)
.and_return(subscription_response)
allow(StripeV2Client).to receive(:request)
.with(:get, '/v2/billing/cadences/cad_123', anything, anything)
.and_return(cadence_response)
# Mock the Stripe gem client methods
stripe_client_double = double
v2_double = double
billing_double = double
subscriptions_double = double
cadences_double = double
allow(service).to receive(:stripe_client).and_return(stripe_client_double)
allow(stripe_client_double).to receive(:v2).and_return(v2_double)
allow(v2_double).to receive(:billing).and_return(billing_double)
allow(billing_double).to receive(:pricing_plan_subscriptions).and_return(subscriptions_double)
allow(billing_double).to receive(:cadences).and_return(cadences_double)
allow(subscriptions_double).to receive(:retrieve).with('bpps_sub_123').and_return(subscription_response)
allow(cadences_double).to receive(:retrieve).with('cad_123').and_return(cadence_response)
end
it 'handles subscription activation' do
# Mock the subscription provisioning service
provisioning_service_double = instance_double(
Enterprise::Billing::V2::SubscriptionProvisioningService,
provision: { pricing_plan_id: pricing_plan_id, quantity: 1 }
)
allow(Enterprise::Billing::V2::SubscriptionProvisioningService).to receive(:new)
.with(account: account)
.and_return(provisioning_service_double)
result = service.perform(event: event)
expect(provisioning_service_double).to have_received(:provision).with(subscription_id: 'bpps_sub_123')
expect(result[:pricing_plan_id]).to eq(pricing_plan_id)
end
end