diff --git a/Gemfile.lock b/Gemfile.lock index 105bf8c13..5db098e7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -901,7 +901,7 @@ GEM squasher (0.7.2) stackprof (0.2.25) statsd-ruby (1.5.0) - stripe (8.5.0) + stripe (16.0.0) telephone_number (1.4.20) test-prof (1.2.1) thor (1.4.0) diff --git a/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb b/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb index 774d63576..505df9936 100644 --- a/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb @@ -3,139 +3,53 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 def fetch_stripe_credit_balance return nil unless stripe_customer_id.present? && v2_enabled? - with_stripe_error_handling do - response, _api_key = stripe_client.execute_request( - :get, - '/v1/billing/credit_grants', - params: { customer: stripe_customer_id, limit: 100 } - ) + grants = Stripe::Billing::CreditGrant.list( + { customer: stripe_customer_id, limit: 100 }, + { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } + ) - parse_credit_grants(response) - end - rescue StandardError => e - Rails.logger.error "Failed to fetch credit grants: #{e.message}" - nil + parse_credit_grants(grants) end def create_stripe_credit_grant(amount, type: 'promotional', metadata: {}) - return nil unless stripe_customer_id.present? + return nil if stripe_customer_id.blank? - params = { - customer: stripe_customer_id, - name: "#{type.titleize} Credits - #{Time.current.strftime('%Y-%m-%d')}", - amount: { - type: 'monetary', - monetary: { - currency: 'usd', - value: amount.to_i - } - }, - category: type == 'topup' ? 'paid' : 'promotional', - applicability_config: { scope: { price_type: 'metered' } }, - metadata: metadata.merge( - account_id: account.id.to_s, - created_by: 'chatwoot_v2', - credit_type: type, - credits: amount.to_s - ) - } - - params[:expiry_config] = { type: 'end_of_service_period' } if type == 'monthly' - - response, _api_key = stripe_client.execute_request(:post, '/v1/billing/credit_grants', params: params) - response.is_a?(Stripe::StripeResponse) ? response.data : response - rescue StandardError => e - Rails.logger.error "Failed to create credit grant: #{e.message}" - nil + params = build_credit_grant_params(amount, type, metadata) + create_stripe_grant(params) end def grant_monthly_credits(amount = 2000, metadata: {}) with_locked_account do expired_amount = expire_current_monthly_credits(metadata: metadata) - stripe_grant = create_stripe_credit_grant(amount, type: 'monthly', metadata: metadata) - grant_id = stripe_grant ? (stripe_grant['id'] || stripe_grant[:id]) : nil - update_credits(monthly: amount) - if amount.positive? - log_credit_transaction( - type: 'grant', - amount: amount, - credit_type: 'monthly', - description: 'Monthly credit grant', - metadata: base_metadata(metadata).merge( - 'expired_amount' => expired_amount, - 'stripe_grant_id' => grant_id - ) - ) - end + log_monthly_grant(amount, expired_amount, stripe_grant&.id, metadata) if amount.positive? { success: true, granted: amount, expired: expired_amount, remaining: total_credits } end - - rescue ActiveRecord::RecordInvalid => e - Rails.logger.error "Failed to grant monthly credits: #{e.message}" - { success: false, message: e.message } end - # rubocop:disable Metrics/MethodLength def use_credit(feature: 'ai_captain', amount: 1, metadata: {}) return { success: true, credits_used: 0, remaining: total_credits } if amount <= 0 - reporter = Enterprise::Billing::V2::UsageReporterService.new(account: account) - stripe_result = reporter.report(amount, feature, metadata) - + 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 - current_balance = credit_balance + return { success: false, message: 'Insufficient credits' } unless sufficient_balance?(amount) - if current_balance[:total] < amount - Rails.logger.warn 'Local cache out of sync with Stripe' - return { success: false, message: 'Insufficient credits' } - end + credit_type = deduct_credits(amount) + log_credit_usage(amount, feature, credit_type, stripe_result[:event_id], metadata) - current_monthly = monthly_credits - current_topup = topup_credits - credit_type = 'monthly' - - if current_monthly >= amount - update_credits(monthly: current_monthly - amount) - else - monthly_used = current_monthly - topup_used = amount - monthly_used - update_credits(monthly: 0, topup: current_topup - topup_used) - credit_type = monthly_used.positive? ? 'mixed' : 'topup' - end - - log_credit_transaction( - type: 'use', - amount: amount, - credit_type: credit_type, - description: "Used for #{feature}", - metadata: base_metadata(metadata).merge( - 'feature' => feature, - 'stripe_event_id' => stripe_result[:event_id] - ) - ) - - final_balance = credit_balance - { - success: true, - credits_used: amount, - remaining: final_balance[:total], - source: final_balance[:source], - stripe_event_id: stripe_result[:event_id] - } + build_credit_usage_result(amount, stripe_result[:event_id]) end end - # rubocop:enable Metrics/MethodLength def add_topup_credits(amount, metadata: {}) with_locked_account do stripe_grant = create_stripe_credit_grant(amount, type: 'topup', metadata: metadata) - grant_id = stripe_grant ? (stripe_grant['id'] || stripe_grant[:id]) : nil + grant_id = stripe_grant&.id new_balance = topup_credits + amount update_credits(topup: new_balance) @@ -150,10 +64,6 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 { success: true, topup_balance: new_balance, total: total_credits } end - - rescue ActiveRecord::RecordInvalid => e - Rails.logger.error "Failed to add topup credits: #{e.message}" - { success: false, message: e.message } end def total_credits @@ -165,33 +75,9 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 initial_credits = initial_credits_from_local if stripe_usage.is_a?(Numeric) && initial_credits - total_used = stripe_usage - total_granted = initial_credits[:total_granted] - remaining = [total_granted - total_used, 0].max - - monthly_portion = [remaining, initial_credits[:monthly_granted]].min - topup_portion = [remaining - monthly_portion, 0].max - - balance = { - monthly: monthly_portion, - topup: topup_portion, - total: remaining, - usage_from_stripe: total_used, - granted_from_stripe: total_granted, - last_synced: Time.current, - source: 'stripe' - } - - sync_local_balance_from_stripe(balance) - balance + calculate_balance_from_stripe(stripe_usage, initial_credits) else - { - monthly: monthly_credits, - topup: topup_credits, - total: total_credits, - last_synced: Time.current, - source: 'local_fallback' - } + local_fallback_balance end end @@ -211,23 +97,19 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 def fetch_stripe_usage_total return nil unless stripe_customer_id.present? && ENV['STRIPE_V2_METER_ID'].present? - response, _api_key = stripe_client.execute_request( - :get, - "/v1/billing/meters/#{ENV.fetch('STRIPE_V2_METER_ID', nil)}/event_summaries", - headers: { 'Stripe-Version' => '2025-08-27.preview' }, - params: { + summaries = Stripe::Billing::Meter.list_event_summaries( + ENV.fetch('STRIPE_V2_METER_ID', nil), + { customer: stripe_customer_id, start_time: Time.current.beginning_of_month.to_i, end_time: Time.current.to_i - } + }, + { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } ) - data = response.is_a?(Stripe::StripeResponse) ? response.data : response - summaries = extract_summaries(data) - - summaries.sum { |s| (s['aggregated_value'] || s[:aggregated_value] || 0).to_i } - rescue StandardError => e - Rails.logger.error "Failed to fetch meter summaries: #{e.message}" + summaries_data = extract_summaries(summaries) + summaries_data.sum { |s| (s['aggregated_value'] || s[:aggregated_value] || 0).to_i } + rescue StandardError nil end @@ -260,62 +142,21 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 end def parse_credit_grants(response) - return nil unless response + grants = extract_grants_from_response(response) + return nil if grants.blank? - data = response.is_a?(Stripe::StripeResponse) ? response.data : response - return nil unless data + totals = { monthly: 0, topup: 0, grant_details: [] } + process_grants(grants, totals) - grants = data.is_a?(Hash) ? (data['data'] || data[:data] || []) : [] - return nil if grants.empty? - - monthly = 0 - topup = 0 - grant_details = [] - - grants.each do |grant| - voided_at = grant['voided_at'] || grant[:voided_at] - next unless voided_at.nil? - - amount_data = grant['amount'] || grant[:amount] - next unless amount_data - - available = extract_grant_amount(amount_data) - category = grant['category'] || grant[:category] - expiry_config = grant['expiry_config'] || grant[:expiry_config] - grant_id = grant['id'] || grant[:id] - - if category == 'paid' || expiry_config.nil? - topup += available - grant_details << { type: 'topup', amount: available, id: grant_id } - else - monthly += available - grant_details << { type: 'monthly', amount: available, id: grant_id, expiry_config: expiry_config } - end - end - - { - monthly: monthly, - topup: topup, - total: monthly + topup, - last_synced: Time.current, - source: 'stripe', - grant_details: grant_details - } + build_credit_grant_summary(totals) end def extract_grant_amount(amount_data) amount_type = amount_data['type'] || amount_data[:type] + return 0 unless amount_type - case amount_type - when 'custom_pricing_unit' - cpu_data = amount_data['custom_pricing_unit'] || amount_data[:custom_pricing_unit] - (cpu_data&.[]('value') || cpu_data&.[](:value) || 0).to_i - when 'monetary' - monetary_data = amount_data['monetary'] || amount_data[:monetary] - (monetary_data&.[]('value') || monetary_data&.[](:value) || 0).to_i - else - 0 - end + value_data = amount_data[amount_type] || amount_data[amount_type.to_sym] + extract_value_from_data(value_data) end def sync_local_balance_from_stripe(stripe_balance) @@ -345,5 +186,184 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 def base_metadata(metadata) metadata.is_a?(Hash) ? metadata.stringify_keys : {} end + + # Helper methods for create_stripe_credit_grant + def build_credit_grant_params(amount, type, metadata) + params = { + customer: stripe_customer_id, + name: "#{type.titleize} Credits - #{Time.current.strftime('%Y-%m-%d')}", + amount: { type: 'monetary', monetary: { currency: 'usd', value: amount.to_i } }, + category: type == 'topup' ? 'paid' : 'promotional', + applicability_config: { scope: { price_type: 'metered' } }, + metadata: metadata.merge( + account_id: account.id.to_s, + created_by: 'chatwoot_v2', + credit_type: type, + credits: amount.to_s + ) + } + params[:expiry_config] = { type: 'end_of_service_period' } if type == 'monthly' + params + end + + def create_stripe_grant(params) + Stripe::Billing::CreditGrant.create( + params, + { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } + ) + end + + # Helper methods for grant_monthly_credits + def log_monthly_grant(amount, expired_amount, grant_id, metadata) + log_credit_transaction( + type: 'grant', + amount: amount, + credit_type: 'monthly', + description: 'Monthly credit grant', + metadata: base_metadata(metadata).merge('expired_amount' => expired_amount, 'stripe_grant_id' => grant_id) + ) + end + + # Helper methods for use_credit + def report_usage_to_stripe(amount, feature, metadata) + reporter = Enterprise::Billing::V2::UsageReporterService.new(account: account) + reporter.report(amount, feature, metadata) + end + + def sufficient_balance?(amount) + current_balance = credit_balance + current_balance[:total] >= amount + end + + def deduct_credits(amount) + current_monthly = monthly_credits + current_topup = topup_credits + + if current_monthly >= amount + update_credits(monthly: current_monthly - amount) + 'monthly' + else + monthly_used = current_monthly + topup_used = amount - monthly_used + update_credits(monthly: 0, topup: current_topup - topup_used) + monthly_used.positive? ? 'mixed' : 'topup' + end + end + + def log_credit_usage(amount, feature, credit_type, event_id, metadata) + log_credit_transaction( + type: 'use', + amount: amount, + credit_type: credit_type, + description: "Used for #{feature}", + metadata: base_metadata(metadata).merge('feature' => feature, 'stripe_event_id' => event_id) + ) + end + + def build_credit_usage_result(amount, event_id) + final_balance = credit_balance + { + success: true, + credits_used: amount, + remaining: final_balance[:total], + source: final_balance[:source], + stripe_event_id: event_id + } + end + + # Helper methods for parse_credit_grants + def extract_grants_from_response(response) + return nil unless response + + data = response.is_a?(Stripe::StripeResponse) ? response.data : response + return nil unless data + + data.is_a?(Hash) ? (data['data'] || data[:data] || []) : [] + end + + def process_grants(grants, totals) + grants.each do |grant| + next unless grant_active?(grant) + + amount_data = grant['amount'] || grant[:amount] + next unless amount_data + + process_single_grant(grant, amount_data, totals) + end + end + + def grant_active?(grant) + voided_at = grant['voided_at'] || grant[:voided_at] + voided_at.nil? + end + + def process_single_grant(grant, amount_data, totals) + available = extract_grant_amount(amount_data) + category = grant['category'] || grant[:category] + expiry_config = grant['expiry_config'] || grant[:expiry_config] + grant_id = grant['id'] || grant[:id] + + if category == 'paid' || expiry_config.nil? + totals[:topup] += available + totals[:grant_details] << { type: 'topup', amount: available, id: grant_id } + else + totals[:monthly] += available + totals[:grant_details] << { type: 'monthly', amount: available, id: grant_id, expiry_config: expiry_config } + end + end + + def build_credit_grant_summary(totals) + { + monthly: totals[:monthly], + topup: totals[:topup], + total: totals[:monthly] + totals[:topup], + last_synced: Time.current, + source: 'stripe', + grant_details: totals[:grant_details] + } + end + + # Helper methods for extract_grant_amount + def extract_value_from_data(value_data) + return 0 unless value_data + + (value_data['value'] || value_data[:value] || 0).to_i + end + + # Helper methods for credit_balance + def calculate_balance_from_stripe(stripe_usage, initial_credits) + total_used = stripe_usage + total_granted = initial_credits[:total_granted] + remaining = [total_granted - total_used, 0].max + + monthly_portion = [remaining, initial_credits[:monthly_granted]].min + topup_portion = [remaining - monthly_portion, 0].max + + balance = build_stripe_balance(monthly_portion, topup_portion, remaining, total_used, total_granted) + sync_local_balance_from_stripe(balance) + balance + end + + def build_stripe_balance(monthly_portion, topup_portion, remaining, total_used, total_granted) + { + monthly: monthly_portion, + topup: topup_portion, + total: remaining, + usage_from_stripe: total_used, + granted_from_stripe: total_granted, + last_synced: Time.current, + source: 'stripe' + } + end + + def local_fallback_balance + { + monthly: monthly_credits, + topup: topup_credits, + total: total_credits, + last_synced: Time.current, + source: 'local_fallback' + } + end end # rubocop:enable Metrics/ClassLength diff --git a/enterprise/app/services/enterprise/billing/v2/usage_analytics_service.rb b/enterprise/app/services/enterprise/billing/v2/usage_analytics_service.rb index f079c166f..01c8d439b 100644 --- a/enterprise/app/services/enterprise/billing/v2/usage_analytics_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/usage_analytics_service.rb @@ -2,15 +2,11 @@ class Enterprise::Billing::V2::UsageAnalyticsService < Enterprise::Billing::V2:: def fetch_usage_summary return { success: false, message: 'Not on V2 billing' } unless v2_enabled? - # ALWAYS use Stripe as primary source stripe_analytics = fetch_stripe_meter_events if stripe_analytics && stripe_analytics[:success] - Rails.logger.info 'Using Stripe meter events as source of truth' - return stripe_analytics + stripe_analytics else - Rails.logger.warn 'Stripe unavailable - using local cache as fallback' - # Only use local as fallback with warning local_summary = fetch_local_usage_summary local_summary[:warning] = 'Using cached data - Stripe unavailable' local_summary @@ -23,67 +19,12 @@ class Enterprise::Billing::V2::UsageAnalyticsService < Enterprise::Billing::V2:: begin end_time = Time.current start_time = end_time.beginning_of_month - meter_id = ENV.fetch('STRIPE_V2_METER_ID', nil) - # Fetch meter event summaries - response = stripe_client.execute_request( - :get, - "/v1/billing/meters/#{meter_id}/event_summaries", - params: { - customer: stripe_customer_id, - start_time: start_time.to_i, - end_time: end_time.to_i - } - ) + summaries = fetch_meter_summaries_from_stripe(start_time, end_time) + return nil unless summaries - # Handle response - it can be an Array directly or a Hash with 'data' - summaries = if response.is_a?(Array) - response - elsif response.is_a?(Hash) && response['data'] - response['data'] - end - - if summaries - # Calculate total usage from summaries - total_usage = 0 - usage_by_day = {} - - summaries.each do |summary| - next unless summary.is_a?(Hash) - - value = summary['aggregated_value'].to_i - total_usage += value - - # Group by day if period info available - next unless summary['period'] && summary['period']['start'] - - day = Time.at(summary['period']['start']).strftime('%Y-%m-%d') - usage_by_day[day] ||= 0 - usage_by_day[day] += value - end - - # Get current balance from credit service - credit_service = Enterprise::Billing::V2::CreditManagementService.new(account: account) - balance = credit_service.credit_balance - - # For feature breakdown, we'll need to track this locally - # since meter summaries don't include feature metadata - usage_by_feature = fetch_local_feature_breakdown(start_time, end_time) - - { - success: true, - total_usage: total_usage, - credits_remaining: balance[:total], - period_start: start_time, - period_end: end_time, - usage_by_feature: usage_by_feature, - usage_by_day: usage_by_day, - event_count: summaries.length, - source: 'stripe_meter_events' - } - end - rescue StandardError => e - Rails.logger.error "Failed to fetch Stripe meter summaries: #{e.message}" + build_usage_analytics_result(summaries, start_time, end_time) + rescue StandardError nil end end @@ -134,40 +75,13 @@ class Enterprise::Billing::V2::UsageAnalyticsService < Enterprise::Billing::V2:: def parse_stripe_analytics(response, start_time, end_time) return nil unless response - data = response.is_a?(Hash) ? response : response.data + data = extract_data_from_response(response) return nil unless data - # Sum up all meter events - total_usage = 0 - usage_by_day = {} + metrics = calculate_usage_metrics_from_events(data) + balance = fetch_credit_balance - if data['data'].is_a?(Array) - data['data'].each do |event| - value = event['value'] || 0 - total_usage += value - - # Group by day if timestamp available - next unless event['timestamp'] - - day = Time.at(event['timestamp']).strftime('%Y-%m-%d') - usage_by_day[day] ||= 0 - usage_by_day[day] += value - end - end - - # Get current credit balance - credit_service = Enterprise::Billing::V2::CreditManagementService.new(account: account) - balance = credit_service.credit_balance - - { - success: true, - total_usage: total_usage, - credits_remaining: balance[:total], - period_start: start_time, - period_end: end_time, - usage_by_day: usage_by_day, - source: 'stripe_analytics' - } + build_analytics_result(metrics, balance, start_time, end_time) end def credit_usage_scope(start_time:, end_time:) @@ -191,4 +105,102 @@ class Enterprise::Billing::V2::UsageAnalyticsService < Enterprise::Billing::V2:: def stripe_customer_id custom_attribute('stripe_customer_id') end + + # Helper methods for fetch_stripe_meter_events + def fetch_meter_summaries_from_stripe(start_time, end_time) + meter_id = ENV.fetch('STRIPE_V2_METER_ID', nil) + response = Stripe::Billing::Meter.list_event_summaries( + meter_id, + { customer: stripe_customer_id, start_time: start_time.to_i, end_time: end_time.to_i }, + { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } + ) + extract_summaries_from_response(response) + end + + def extract_summaries_from_response(response) + return response if response.is_a?(Array) + return response.data if response.respond_to?(:data) + return response['data'] if response.is_a?(Hash) && response['data'] + + nil + end + + def build_usage_analytics_result(summaries, start_time, end_time) + metrics = calculate_usage_metrics(summaries) + balance = fetch_credit_balance + usage_by_feature = fetch_local_feature_breakdown(start_time, end_time) + + { + success: true, + total_usage: metrics[:total_usage], + credits_remaining: balance[:total], + period_start: start_time, + period_end: end_time, + usage_by_feature: usage_by_feature, + usage_by_day: metrics[:usage_by_day], + event_count: summaries.length, + source: 'stripe_meter_events' + } + end + + def calculate_usage_metrics(summaries) + total_usage = 0 + usage_by_day = {} + + summaries.each do |summary| + next unless summary.is_a?(Hash) + + value = summary['aggregated_value'].to_i + total_usage += value + + next unless summary['period']&.[]('start') + + day = Time.zone.at(summary['period']['start']).strftime('%Y-%m-%d') + usage_by_day[day] ||= 0 + usage_by_day[day] += value + end + + { total_usage: total_usage, usage_by_day: usage_by_day } + end + + def fetch_credit_balance + Enterprise::Billing::V2::CreditManagementService.new(account: account).credit_balance + end + + # Helper methods for parse_stripe_analytics + def extract_data_from_response(response) + response.is_a?(Hash) ? response : response.data + end + + def calculate_usage_metrics_from_events(data) + total_usage = 0 + usage_by_day = {} + + return { total_usage: total_usage, usage_by_day: usage_by_day } unless data['data'].is_a?(Array) + + data['data'].each do |event| + value = event['value'] || 0 + total_usage += value + + next unless event['timestamp'] + + day = Time.zone.at(event['timestamp']).strftime('%Y-%m-%d') + usage_by_day[day] ||= 0 + usage_by_day[day] += value + end + + { total_usage: total_usage, usage_by_day: usage_by_day } + end + + def build_analytics_result(metrics, balance, start_time, end_time) + { + success: true, + total_usage: metrics[:total_usage], + credits_remaining: balance[:total], + period_start: start_time, + period_end: end_time, + usage_by_day: metrics[:usage_by_day], + source: 'stripe_analytics' + } + 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 c0647a30b..2b7efed71 100644 --- a/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/usage_reporter_service.rb @@ -12,30 +12,9 @@ class Enterprise::Billing::V2::UsageReporterService < Enterprise::Billing::V2::B def report(credits_used, feature, _metadata = {}) return { success: false, message: 'Usage reporting disabled' } unless should_report_usage? - identifier = usage_identifier(feature) - - # Stripe V2 meter events API - response, _api_key = stripe_client.execute_request( - :post, - '/v1/billing/meter_events', - headers: { 'Stripe-Version' => '2025-08-27.preview' }, - params: { - :event_name => meter_event_name, - 'payload[value]' => credits_used.to_s, - 'payload[stripe_customer_id]' => stripe_customer_id, - :identifier => identifier - } - ) - - event_id = response.data[:identifier] - Rails.logger.info "Usage reported: #{credits_used} credits for #{feature} (#{event_id})" - - { success: true, event_id: event_id, reported_credits: credits_used } - rescue Stripe::StripeError => e - Rails.logger.error "Stripe usage reporting failed: #{e.message}" - { success: false, message: e.message } + meter_event = create_meter_event(credits_used, feature) + { success: true, event_id: meter_event.identifier, reported_credits: credits_used } rescue StandardError => e - Rails.logger.error "Usage reporting error: #{e.message}" { success: false, message: e.message } end @@ -60,4 +39,16 @@ class Enterprise::Billing::V2::UsageReporterService < Enterprise::Billing::V2::B def stripe_customer_id custom_attribute('stripe_customer_id') end + + def create_meter_event(credits_used, feature) + identifier = usage_identifier(feature) + Stripe::Billing::MeterEvent.create( + { + event_name: meter_event_name, + payload: { value: credits_used.to_s, stripe_customer_id: stripe_customer_id }, + identifier: identifier + }, + { api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' } + ) + end end