mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 11:37:58 +00:00
fix rspec and customer subscription process
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Enterprise::Billing::V2::Concerns::PaymentIntentHandler
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_payment_if_needed(intent, intent_id)
|
||||||
|
amount_due = intent.amount_details&.total || intent.amount_details.total
|
||||||
|
return nil unless amount_due&.to_i&.positive?
|
||||||
|
|
||||||
|
payment_method_id = fetch_default_payment_method
|
||||||
|
create_upfront_payment_intent(amount_due, intent.currency, payment_method_id, intent_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_default_payment_method
|
||||||
|
customer = Stripe::Customer.retrieve(
|
||||||
|
@customer_id,
|
||||||
|
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||||
|
)
|
||||||
|
customer.invoice_settings&.default_payment_method
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_upfront_payment_intent(amount_due, currency, payment_method_id, intent_id)
|
||||||
|
payment_intent = Stripe::PaymentIntent.create(
|
||||||
|
payment_intent_params(amount_due, currency, payment_method_id, intent_id),
|
||||||
|
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||||
|
)
|
||||||
|
|
||||||
|
Rails.logger.info("Created payment intent: #{payment_intent.id} for amount: #{amount_due}")
|
||||||
|
payment_intent.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def payment_intent_params(amount_due, currency, payment_method_id, intent_id)
|
||||||
|
{
|
||||||
|
amount: amount_due,
|
||||||
|
currency: currency || 'usd',
|
||||||
|
customer: @customer_id,
|
||||||
|
payment_method: payment_method_id,
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
allow_redirects: 'never'
|
||||||
|
},
|
||||||
|
confirm: true,
|
||||||
|
off_session: true,
|
||||||
|
metadata: { billing_intent_id: intent_id }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_billing_intent(intent_id)
|
||||||
|
StripeV2Client.request(
|
||||||
|
:get,
|
||||||
|
"/v2/billing/intents/#{intent_id}",
|
||||||
|
{},
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit_billing_intent(intent_id, payment_intent_id)
|
||||||
|
commit_params = payment_intent_id ? { payment_intent: payment_intent_id } : {}
|
||||||
|
|
||||||
|
StripeV2Client.request(
|
||||||
|
:post,
|
||||||
|
"/v2/billing/intents/#{intent_id}/commit",
|
||||||
|
commit_params,
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,12 +20,12 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
|||||||
def use_credit(feature: 'ai_captain', amount: 1, metadata: {})
|
def use_credit(feature: 'ai_captain', amount: 1, metadata: {})
|
||||||
return { success: true, credits_used: 0, remaining: total_credits } if amount <= 0
|
return { success: true, credits_used: 0, remaining: total_credits } if amount <= 0
|
||||||
|
|
||||||
stripe_result = report_usage_to_stripe(amount, feature, metadata)
|
|
||||||
return { success: false, message: "Usage reporting failed: #{stripe_result[:message]}" } unless stripe_result[:success]
|
|
||||||
|
|
||||||
with_locked_account do
|
with_locked_account do
|
||||||
return { success: false, message: 'Insufficient credits' } unless sufficient_balance?(amount)
|
return { success: false, message: 'Insufficient credits' } unless sufficient_balance?(amount)
|
||||||
|
|
||||||
|
stripe_result = report_usage_to_stripe(amount, feature, metadata)
|
||||||
|
return { success: false, message: "Usage reporting failed: #{stripe_result[:message]}" } unless stripe_result[:success]
|
||||||
|
|
||||||
credit_type = deduct_credits(amount)
|
credit_type = deduct_credits(amount)
|
||||||
log_credit_usage(amount, feature, credit_type, stripe_result[:event_id], metadata)
|
log_credit_usage(amount, feature, credit_type, stripe_result[:event_id], metadata)
|
||||||
build_credit_usage_result(amount, stripe_result[:event_id])
|
build_credit_usage_result(amount, stripe_result[:event_id])
|
||||||
@@ -113,8 +113,8 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
|||||||
end
|
end
|
||||||
|
|
||||||
def sufficient_balance?(amount)
|
def sufficient_balance?(amount)
|
||||||
current_balance = credit_balance
|
# Use local balance (real-time) instead of Stripe balance (5-10 min delay)
|
||||||
current_balance[:total] >= amount
|
total_credits >= amount
|
||||||
end
|
end
|
||||||
|
|
||||||
def deduct_credits(amount)
|
def deduct_credits(amount)
|
||||||
@@ -143,12 +143,12 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_credit_usage_result(amount, event_id)
|
def build_credit_usage_result(amount, event_id)
|
||||||
final_balance = credit_balance
|
# Use local balance (already deducted) instead of fetching from Stripe
|
||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
credits_used: amount,
|
credits_used: amount,
|
||||||
remaining: final_balance[:total],
|
remaining: total_credits,
|
||||||
source: final_balance[:source],
|
source: 'local',
|
||||||
stripe_event_id: event_id
|
stripe_event_id: event_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -87,25 +87,37 @@ class Enterprise::Billing::V2::PricingPlanComponentBuilder < Enterprise::Billing
|
|||||||
StripeV2Client.request(
|
StripeV2Client.request(
|
||||||
:post,
|
:post,
|
||||||
'/v2/billing/service_actions',
|
'/v2/billing/service_actions',
|
||||||
{
|
service_action_params(lookup_key, credit_amount, cpu_id),
|
||||||
lookup_key: lookup_key,
|
stripe_api_options
|
||||||
service_interval: 'month',
|
|
||||||
service_interval_count: 1,
|
|
||||||
type: 'credit_grant',
|
|
||||||
credit_grant: {
|
|
||||||
name: 'Monthly Credits',
|
|
||||||
amount: {
|
|
||||||
type: 'custom_pricing_unit',
|
|
||||||
custom_pricing_unit: { id: cpu_id, value: credit_amount.to_s }
|
|
||||||
},
|
|
||||||
expiry_config: { type: 'end_of_service_period' },
|
|
||||||
applicability_config: { scope: { price_type: 'metered' } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def service_action_params(lookup_key, credit_amount, cpu_id)
|
||||||
|
{
|
||||||
|
lookup_key: lookup_key,
|
||||||
|
service_interval: 'month',
|
||||||
|
service_interval_count: 1,
|
||||||
|
type: 'credit_grant',
|
||||||
|
credit_grant: credit_grant_config(credit_amount, cpu_id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def credit_grant_config(credit_amount, cpu_id)
|
||||||
|
{
|
||||||
|
name: 'Monthly Credits',
|
||||||
|
amount: {
|
||||||
|
type: 'custom_pricing_unit',
|
||||||
|
custom_pricing_unit: { id: cpu_id, value: credit_amount.to_s }
|
||||||
|
},
|
||||||
|
expiry_config: { type: 'end_of_service_period' },
|
||||||
|
applicability_config: { scope: { price_type: 'metered' } }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stripe_api_options
|
||||||
|
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||||
|
end
|
||||||
|
|
||||||
def create_rate_card(display_name:)
|
def create_rate_card(display_name:)
|
||||||
StripeV2Client.request(
|
StripeV2Client.request(
|
||||||
:post,
|
:post,
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class Enterprise::Billing::V2::StripeCreditSyncService < Enterprise::Billing::V2
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_credit_grant_params(amount, type, metadata)
|
def build_credit_grant_params(amount, type, metadata)
|
||||||
params = {
|
{
|
||||||
customer: stripe_customer_id,
|
customer: stripe_customer_id,
|
||||||
name: "#{type.titleize} Credits - #{Time.current.strftime('%Y-%m-%d')}",
|
name: "#{type.titleize} Credits - #{Time.current.strftime('%Y-%m-%d')}",
|
||||||
amount: { type: 'monetary', monetary: { currency: 'usd', value: amount.to_i } },
|
amount: { type: 'monetary', monetary: { currency: 'usd', value: amount.to_i } },
|
||||||
@@ -172,8 +172,6 @@ class Enterprise::Billing::V2::StripeCreditSyncService < Enterprise::Billing::V2
|
|||||||
credits: amount.to_s
|
credits: amount.to_s
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params[:expiry_config] = { type: 'end_of_service_period' } if type == 'monthly'
|
|
||||||
params
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_stripe_grant(params)
|
def create_stripe_grant(params)
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
class Enterprise::Billing::V2::SubscribeCustomerService < Enterprise::Billing::V2::BaseService
|
||||||
|
include Enterprise::Billing::V2::Concerns::PaymentIntentHandler
|
||||||
|
|
||||||
|
def subscribe_to_pricing_plan(pricing_plan_id:, customer_id: nil)
|
||||||
|
@pricing_plan_id = pricing_plan_id
|
||||||
|
@customer_id = customer_id || stripe_customer_id
|
||||||
|
|
||||||
|
validate_subscription_params
|
||||||
|
execute_subscription_flow
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
{ success: false, message: "Stripe API error: #{e.message}", error: e }
|
||||||
|
rescue StandardError => e
|
||||||
|
{ success: false, message: "Subscription error: #{e.message}", error: e }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_subscription_params
|
||||||
|
return { success: false, message: 'Customer ID required' } if @customer_id.blank?
|
||||||
|
return { success: false, message: 'Pricing Plan ID required' } if @pricing_plan_id.blank?
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_subscription_flow
|
||||||
|
with_locked_account do
|
||||||
|
cadence = create_billing_cadence
|
||||||
|
return { success: false, message: 'Failed to create billing cadence' } unless cadence
|
||||||
|
|
||||||
|
pricing_plan = pricing_plan_details
|
||||||
|
return { success: false, message: 'Failed to get pricing plan details' } unless pricing_plan
|
||||||
|
|
||||||
|
intent = create_billing_intent(cadence.id, pricing_plan)
|
||||||
|
return { success: false, message: 'Failed to create billing intent' } unless intent
|
||||||
|
|
||||||
|
reserve_and_commit_intent(intent.id)
|
||||||
|
update_account_subscription_info(pricing_plan)
|
||||||
|
|
||||||
|
build_subscription_result(cadence.id, intent.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reserve_and_commit_intent(intent_id)
|
||||||
|
reserved_intent = reserve_intent(intent_id)
|
||||||
|
return { success: false, message: 'Failed to reserve intent' } unless reserved_intent
|
||||||
|
|
||||||
|
committed_intent = commit_intent(intent_id)
|
||||||
|
return { success: false, message: 'Failed to commit intent' } unless committed_intent
|
||||||
|
|
||||||
|
committed_intent
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_subscription_result(cadence_id, intent_id)
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
customer_id: @customer_id,
|
||||||
|
pricing_plan_id: @pricing_plan_id,
|
||||||
|
cadence_id: cadence_id,
|
||||||
|
intent_id: intent_id,
|
||||||
|
status: 'subscribed'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def stripe_customer_id
|
||||||
|
custom_attribute('stripe_customer_id')
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_billing_cadence
|
||||||
|
cadence_params = {
|
||||||
|
payer: {
|
||||||
|
type: 'customer',
|
||||||
|
customer: @customer_id
|
||||||
|
},
|
||||||
|
billing_cycle: {
|
||||||
|
type: 'month',
|
||||||
|
interval_count: 1,
|
||||||
|
month: {
|
||||||
|
day_of_month: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StripeV2Client.request(
|
||||||
|
:post,
|
||||||
|
'/v2/billing/cadences',
|
||||||
|
cadence_params,
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pricing_plan_details
|
||||||
|
StripeV2Client.request(
|
||||||
|
:get,
|
||||||
|
"/v2/billing/pricing_plans/#{@pricing_plan_id}",
|
||||||
|
{},
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_billing_intent(cadence_id, pricing_plan)
|
||||||
|
plan_version = extract_plan_version(pricing_plan)
|
||||||
|
intent_params = build_intent_params(cadence_id, plan_version)
|
||||||
|
|
||||||
|
StripeV2Client.request(
|
||||||
|
:post,
|
||||||
|
'/v2/billing/intents',
|
||||||
|
intent_params,
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_plan_version(pricing_plan)
|
||||||
|
pricing_plan['latest_version'] || pricing_plan['live_version'] || pricing_plan['version']
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_intent_params(cadence_id, plan_version)
|
||||||
|
{
|
||||||
|
currency: 'usd',
|
||||||
|
cadence: cadence_id,
|
||||||
|
actions: [build_subscription_action(plan_version)]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_subscription_action(plan_version)
|
||||||
|
{
|
||||||
|
type: 'subscribe',
|
||||||
|
subscribe: {
|
||||||
|
type: 'pricing_plan_subscription_details',
|
||||||
|
pricing_plan_subscription_details: {
|
||||||
|
pricing_plan: @pricing_plan_id,
|
||||||
|
pricing_plan_version: plan_version,
|
||||||
|
component_configurations: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def reserve_intent(intent_id)
|
||||||
|
StripeV2Client.request(
|
||||||
|
:post,
|
||||||
|
"/v2/billing/intents/#{intent_id}/reserve",
|
||||||
|
{},
|
||||||
|
stripe_api_options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit_intent(intent_id)
|
||||||
|
ensure_payment_method
|
||||||
|
|
||||||
|
intent = fetch_billing_intent(intent_id)
|
||||||
|
payment_intent_id = create_payment_if_needed(intent, intent_id)
|
||||||
|
|
||||||
|
commit_billing_intent(intent_id, payment_intent_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_payment_method
|
||||||
|
# Check if customer already has a payment method
|
||||||
|
customer = Stripe::Customer.retrieve(
|
||||||
|
@customer_id,
|
||||||
|
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||||
|
)
|
||||||
|
|
||||||
|
return if customer.invoice_settings&.default_payment_method.present?
|
||||||
|
|
||||||
|
# In production, payment methods must be added via Checkout or SetupIntent
|
||||||
|
# This ensures proper customer authentication and PCI compliance
|
||||||
|
raise Stripe::StripeError,
|
||||||
|
'Payment method required. Customer must add payment method via Stripe Checkout or SetupIntent before subscribing.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_account_subscription_info(pricing_plan)
|
||||||
|
update_custom_attributes(
|
||||||
|
'stripe_billing_version' => 2,
|
||||||
|
'stripe_customer_id' => @customer_id,
|
||||||
|
'stripe_pricing_plan_id' => @pricing_plan_id,
|
||||||
|
'plan_name' => extract_plan_name(pricing_plan),
|
||||||
|
'subscription_status' => 'active'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_plan_name(pricing_plan)
|
||||||
|
display_name = pricing_plan['display_name'] || pricing_plan[:display_name]
|
||||||
|
return 'Business' unless display_name
|
||||||
|
|
||||||
|
# Extract plan name from display name like "Chatwoot Business - 2000 Credits"
|
||||||
|
display_name.split('-').first.strip.split.last || 'Business'
|
||||||
|
end
|
||||||
|
|
||||||
|
def stripe_api_options
|
||||||
|
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -45,6 +45,12 @@ module StripeV2Client
|
|||||||
def parse_response(response)
|
def parse_response(response)
|
||||||
body = JSON.parse(response.body)
|
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)
|
# Convert to OpenStruct for dot notation access (mimicking Stripe SDK objects)
|
||||||
case body
|
case body
|
||||||
when Hash
|
when Hash
|
||||||
|
|||||||
@@ -6,11 +6,23 @@ describe Enterprise::Billing::V2::CreditManagementService do
|
|||||||
|
|
||||||
before do
|
before do
|
||||||
allow(Enterprise::Billing::ReportUsageJob).to receive(:perform_later)
|
allow(Enterprise::Billing::ReportUsageJob).to receive(:perform_later)
|
||||||
|
allow(ENV).to receive(:fetch).and_call_original
|
||||||
|
allow(ENV).to receive(:fetch).with('STRIPE_V2_METER_EVENT_NAME', anything).and_return('ai_prompts')
|
||||||
|
allow(ENV).to receive(:[]).and_call_original
|
||||||
|
allow(ENV).to receive(:[]).with('STRIPE_V2_METER_EVENT_NAME').and_return('ai_prompts')
|
||||||
|
allow(ENV).to receive(:[]).with('STRIPE_V2_METER_ID').and_return(nil) # Disable Stripe meter fetching
|
||||||
|
|
||||||
|
# Stub Stripe credit grant creation
|
||||||
|
allow(Stripe::Billing::CreditGrant).to receive(:create).and_return(
|
||||||
|
OpenStruct.new(id: 'cg_test_123')
|
||||||
|
)
|
||||||
|
|
||||||
account.update!(
|
account.update!(
|
||||||
custom_attributes: (account.custom_attributes || {}).merge(
|
custom_attributes: (account.custom_attributes || {}).merge(
|
||||||
'stripe_billing_version' => 2,
|
'stripe_billing_version' => 2,
|
||||||
'monthly_credits' => 100,
|
'monthly_credits' => 100,
|
||||||
'topup_credits' => 50
|
'topup_credits' => 50,
|
||||||
|
'stripe_customer_id' => 'cus_test_123'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -26,6 +38,13 @@ describe Enterprise::Billing::V2::CreditManagementService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#use_credit' do
|
describe '#use_credit' do
|
||||||
|
before do
|
||||||
|
# Stub the Stripe meter event creation
|
||||||
|
allow(Stripe::Billing::MeterEvent).to receive(:create).and_return(
|
||||||
|
OpenStruct.new(identifier: 'test_event_123')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when sufficient monthly credits' do
|
context 'when sufficient monthly credits' do
|
||||||
it 'uses monthly credits first' do
|
it 'uses monthly credits first' do
|
||||||
result = service.use_credit(feature: 'ai_test', amount: 10)
|
result = service.use_credit(feature: 'ai_test', amount: 10)
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ describe Enterprise::Billing::V2::UsageAnalyticsService do
|
|||||||
metadata: {},
|
metadata: {},
|
||||||
created_at: Time.current
|
created_at: Time.current
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Stub the Stripe API call to return nil (which will fallback to local data)
|
||||||
|
allow(ENV).to receive(:[]).and_call_original
|
||||||
|
allow(ENV).to receive(:[]).with('STRIPE_V2_METER_ID').and_return(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'aggregates usage from credit transactions' do
|
it 'aggregates usage from credit transactions' do
|
||||||
|
|||||||
@@ -20,22 +20,22 @@ describe Enterprise::Billing::V2::UsageReporterService do
|
|||||||
|
|
||||||
after { config.replace(original_config) }
|
after { config.replace(original_config) }
|
||||||
|
|
||||||
it 'posts usage events to Stripe meters' do # rubocop:disable RSpec/MultipleExpectations
|
it 'posts usage events to Stripe meters' do
|
||||||
response = { 'id' => 'me_test_123' }
|
meter_event = OpenStruct.new(identifier: 'me_test_123')
|
||||||
stripe_client = instance_double(Stripe::StripeClient)
|
allow(Stripe::Billing::MeterEvent).to receive(:create).and_return(meter_event)
|
||||||
allow(service).to receive(:stripe_client).and_return(stripe_client)
|
|
||||||
allow(stripe_client).to receive(:execute_request).and_return(response)
|
# Stub ENV to return the configured meter event name from config
|
||||||
|
allow(ENV).to receive(:[]).and_call_original
|
||||||
|
allow(ENV).to receive(:[]).with('STRIPE_V2_METER_EVENT_NAME').and_return(nil)
|
||||||
|
|
||||||
result = service.report(5, 'ai_test')
|
result = service.report(5, 'ai_test')
|
||||||
|
|
||||||
expect(result).to include(success: true, reported_credits: 5)
|
expect(result).to include(success: true, reported_credits: 5)
|
||||||
expect(stripe_client).to have_received(:execute_request) do |method, path, **kw|
|
expect(Stripe::Billing::MeterEvent).to have_received(:create) do |params, options|
|
||||||
expect(method).to eq(:post)
|
expect(params[:event_name]).to eq('chat_prompts') # Should use the config value
|
||||||
expect(path).to eq('/v1/billing/meter_events')
|
expect(params[:payload][:value]).to eq('5')
|
||||||
expect(kw[:headers]).to include('Idempotency-Key')
|
expect(params[:payload][:stripe_customer_id]).to eq('cus_test_123')
|
||||||
expect(kw[:params][:event_name]).to eq('chat_prompts')
|
expect(options[:stripe_version]).to eq('2025-08-27.preview')
|
||||||
expect(kw[:params][:payload][:value]).to eq(5)
|
|
||||||
expect(kw[:params][:payload][:stripe_customer_id]).to eq('cus_test_123')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user