From 6abecdfd435cf77bc4a4002309878d852ae05335 Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Mon, 17 Nov 2025 11:15:21 +0530 Subject: [PATCH 1/5] update the logic to get the number of credits for a monthly plan --- .../billing/concerns/plan_provisioning_helper.rb | 10 ---------- .../enterprise/billing/handle_stripe_event_service.rb | 11 ++++++++++- .../services/enterprise/billing/v2/plan_catalog.rb | 5 ----- .../billing/v2/subscription_provisioning_service.rb | 4 +++- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb index cb5447c48..d2cb238ad 100644 --- a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb +++ b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb @@ -4,8 +4,6 @@ module Enterprise::Billing::Concerns::PlanProvisioningHelper private def provision_new_plan(new_pricing_plan_id) - sync_plan_credits(new_pricing_plan_id) - plan_definition = Enterprise::Billing::V2::PlanCatalog.definition_for(new_pricing_plan_id) return unless plan_definition @@ -13,14 +11,6 @@ module Enterprise::Billing::Concerns::PlanProvisioningHelper enable_plan_specific_features(plan_name) if plan_name.present? end - def sync_plan_credits(pricing_plan_id) - plan_credits = Enterprise::Billing::V2::PlanCatalog.monthly_credits_for(pricing_plan_id) - - Enterprise::Billing::V2::CreditManagementService - .new(account: account) - .sync_monthly_response_credits(plan_credits.to_i) - end - def extract_plan_name(plan_definition) plan_definition[:display_name].split.find { |word| %w[Startup Startups Business Enterprise].include?(word) } end diff --git a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb index a1baa7b0f..fac927eef 100644 --- a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb +++ b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb @@ -105,7 +105,16 @@ class Enterprise::Billing::HandleStripeEventService return credits.to_i if credits.present? && credits.to_i.positive? end - 0 + amount = extract_attribute(grant, :amount) + return 0 if amount.blank? + + custom_pricing_unit = extract_attribute(amount, :custom_pricing_unit) + return 0 if custom_pricing_unit.blank? + + value = extract_attribute(custom_pricing_unit, :value) + return 0 if value.blank? + + value.to_i end def extract_attribute(object, attribute) diff --git a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb index c0d9583df..12655104e 100644 --- a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb +++ b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb @@ -53,11 +53,6 @@ module Enterprise::Billing::V2::PlanCatalog nil end - def monthly_credits_for(plan_id) - definition = definition_for(plan_id) - definition ? definition[:monthly_credits] : nil - end - def plan_id_for(definition) InstallationConfig.find_by(name: definition[:config_key])&.value end diff --git a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb index 6b0018e0d..9bdc106ec 100644 --- a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb @@ -63,7 +63,9 @@ class Enterprise::Billing::V2::SubscriptionProvisioningService < Enterprise::Bil update_custom_attributes(attributes) # Sync credits for Hacker plan (0 credits) - sync_plan_credits(pricing_plan_id) + Enterprise::Billing::V2::CreditManagementService + .new(account: account) + .sync_monthly_response_credits(0) # Disable all premium features and save disable_all_premium_features From 689525ce853b3f84548aa5329058a6c0dfeb0cd5 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 17 Nov 2025 11:57:27 +0530 Subject: [PATCH 2/5] fix credit addition for topup and monthly --- app/helpers/billing_helper.rb | 5 ++++- .../billing/handle_stripe_event_service.rb | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/helpers/billing_helper.rb b/app/helpers/billing_helper.rb index e2ada7e86..65c0b55b5 100644 --- a/app/helpers/billing_helper.rb +++ b/app/helpers/billing_helper.rb @@ -8,7 +8,10 @@ module BillingHelper # Return false if not plans are configured, so that no checks are enforced return false if default_plan.blank? - account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan['name'] + # Handle both string and hash formats for default_plan + default_plan_name = default_plan.is_a?(Hash) ? default_plan['name'] : default_plan + + account.custom_attributes['plan_name'].nil? || account.custom_attributes['plan_name'] == default_plan_name end def conversations_this_month(account) diff --git a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb index fac927eef..4d2978b07 100644 --- a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb +++ b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb @@ -89,14 +89,29 @@ class Enterprise::Billing::HandleStripeEventService amount = extract_credit_amount(grant) return if amount.zero? + grant_type = extract_grant_type(grant) service = Enterprise::Billing::V2::CreditManagementService.new(account: account) - service.add_response_topup_credits(amount) + if grant_type == 'monetary' + service.add_response_topup_credits(amount) + else + service.sync_monthly_response_credits(amount) + end end def extract_credit_grant_id(grant_object) grant_object.respond_to?(:id) ? grant_object.id : grant_object['id'] end + def extract_grant_type(grant) + amount_object = extract_attribute(grant, :amount) + return 'monetary' if amount_object.blank? + + type = extract_attribute(amount_object, :type) + return 'monetary' if type.blank? + + type + end + def extract_credit_amount(grant) # First, try to get credits from metadata metadata = extract_attribute(grant, :metadata) @@ -104,7 +119,6 @@ class Enterprise::Billing::HandleStripeEventService credits = extract_attribute(metadata, :credits) return credits.to_i if credits.present? && credits.to_i.positive? end - amount = extract_attribute(grant, :amount) return 0 if amount.blank? From 01d17674e6435326447f7da04a8935da5ec108e0 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Wed, 19 Nov 2025 19:39:40 +0530 Subject: [PATCH 3/5] fix comments --- config/locales/en.yml | 11 +++ config/routes.rb | 16 ++-- .../api/v1/accounts/concerns/billing_v2.rb | 62 -------------- .../enterprise/api/v1/accounts_controller.rb | 1 - .../enterprise/api/v2/billing_controller.rb | 80 +++++++++++++++++++ .../enterprise/webhooks/stripe_controller.rb | 6 +- .../billing/concerns/plan_feature_manager.rb | 5 +- .../concerns/plan_provisioning_helper.rb | 2 +- .../concerns/stripe_v2_client_helper.rb | 4 +- .../billing/create_stripe_customer_service.rb | 2 +- .../enterprise/billing/v2/base_service.rb | 16 +--- .../billing/v2/checkout_session_service.rb | 6 +- .../billing/v2/credit_management_service.rb | 2 +- .../enterprise/billing/v2/plan_catalog.rb | 2 +- .../v2/subscription_provisioning_service.rb | 2 + .../billing/v2/webhook_handler_service.rb | 11 +-- 16 files changed, 127 insertions(+), 101 deletions(-) delete mode 100644 enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb create mode 100644 enterprise/app/controllers/enterprise/api/v2/billing_controller.rb diff --git a/config/locales/en.yml b/config/locales/en.yml index d0f256146..ea7d7c678 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -119,6 +119,12 @@ en: invalid_token: Invalid or expired MFA token invalid_credentials: Invalid credentials or verification code feature_unavailable: MFA feature is not available. Please configure encryption keys. + enterprise: + billing: + topup_amount_invalid: Topup amount must be greater than 0 + stripe_customer_required: Customer ID required. Please create a Stripe customer first. + lookup_key_not_found: Lookup key not found for pricing plan %{pricing_plan_id} + v2_configuration_required: V2 billing configuration is required. Please configure STRIPE_HACKER_PLAN_ID. profile: mfa: enabled: MFA enabled successfully @@ -435,3 +441,8 @@ en: subject: 'Finish setting up %{custom_domain}' ssl_status: custom_domain_not_configured: 'Custom domain is not configured' + enterprise: + billing: + topup_successful: Topup successful + subscription_cancelled: Subscription cancelled + pricing_plan_changed: Pricing plan changed diff --git a/config/routes.rb b/config/routes.rb index d886d004a..4d9ef8221 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -431,12 +431,18 @@ Rails.application.routes.draw do post :subscription get :limits post :toggle_deletion - # V2 Billing endpoints + end + end + end + + namespace :v2 do + resources :accounts, only: [] do + resource :billing, only: [] do get :credit_grants - get :v2_pricing_plans - get :v2_topup_options - post :v2_topup - post :v2_subscribe + get :pricing_plans + get :topup_options + post :topup + post :subscribe post :cancel_subscription post :change_pricing_plan end diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb deleted file mode 100644 index c20c5f54c..000000000 --- a/enterprise/app/controllers/enterprise/api/v1/accounts/concerns/billing_v2.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Enterprise::Api::V1::Accounts::Concerns::BillingV2 - extend ActiveSupport::Concern - - included do - before_action :validate_topup_amount, only: [:v2_topup] - end - - def credit_grants - service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) - grants = service.fetch_credit_grants - - render json: { credit_grants: grants } - end - - def v2_pricing_plans - plans = Enterprise::Billing::V2::PlanCatalog.plans - render json: { pricing_plans: plans } - end - - def v2_topup_options - options = Enterprise::Billing::V2::TopupCatalog.options - render json: { topup_options: options } - end - - def v2_topup - render json: { success: true, message: 'Topup successful.' } - end - - def v2_subscribe - service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account) - result = service.create_subscription_checkout( - pricing_plan_id: params[:pricing_plan_id], - quantity: subscription_quantity - ) - - if result[:success] - render json: { success: true, redirect_url: result[:redirect_url] } - else - render json: { error: result[:message] }, status: :unprocessable_entity - end - end - - def cancel_subscription - render json: { success: true, message: 'Subscription cancelled.' } - end - - def change_pricing_plan - render json: { success: true, message: 'Pricing plan changed.' } - end - - private - - def subscription_quantity - [params[:quantity].to_i, 1].max - end - - def validate_topup_amount - return if params[:credits].to_i.positive? - - render json: { error: 'Topup amount must be greater than 0' }, status: :unprocessable_entity - end -end diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb index 7fed4dfcf..77791cf4d 100644 --- a/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb +++ b/enterprise/app/controllers/enterprise/api/v1/accounts_controller.rb @@ -1,6 +1,5 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController include BillingHelper - include Enterprise::Api::V1::Accounts::Concerns::BillingV2 before_action :fetch_account before_action :check_authorization diff --git a/enterprise/app/controllers/enterprise/api/v2/billing_controller.rb b/enterprise/app/controllers/enterprise/api/v2/billing_controller.rb new file mode 100644 index 000000000..892b6ea10 --- /dev/null +++ b/enterprise/app/controllers/enterprise/api/v2/billing_controller.rb @@ -0,0 +1,80 @@ +class Enterprise::Api::V2::BillingController < Api::BaseController + before_action :fetch_account + before_action :check_authorization + before_action :validate_topup_amount, only: [:topup] + + rescue_from StandardError, with: :render_error + rescue_from NotImplementedError, with: :render_not_implemented + + def credit_grants + service = Enterprise::Billing::V2::CreditManagementService.new(account: @account) + grants = service.fetch_credit_grants + + render json: { credit_grants: grants } + end + + def pricing_plans + plans = Enterprise::Billing::V2::PlanCatalog.plans + render json: { pricing_plans: plans } + end + + def topup_options + options = Enterprise::Billing::V2::TopupCatalog.options + render json: { topup_options: options } + end + + def topup + raise NotImplementedError, 'Topup functionality not yet implemented' + end + + def subscribe + service = Enterprise::Billing::V2::CheckoutSessionService.new(account: @account) + redirect_url = service.create_subscription_checkout( + pricing_plan_id: params[:pricing_plan_id], + quantity: subscription_quantity + ) + + render json: { redirect_url: redirect_url } + end + + def cancel_subscription + raise NotImplementedError, 'Cancel subscription functionality not yet implemented' + end + + def change_pricing_plan + raise NotImplementedError, 'Change pricing plan functionality not yet implemented' + end + + private + + def fetch_account + @account = current_user.accounts.find(params[:account_id]) + @current_account_user = @account.account_users.find_by(user_id: current_user.id) + end + + def subscription_quantity + [params[:quantity].to_i, 1].max + end + + def validate_topup_amount + return if params[:credits].to_i.positive? + + render json: { error: I18n.t('errors.enterprise.billing.topup_amount_invalid') }, status: :unprocessable_entity + end + + def pundit_user + { + user: current_user, + account: @account, + account_user: @current_account_user + } + end + + def render_error(exception) + render json: { error: exception.message }, status: :unprocessable_entity + end + + def render_not_implemented(exception) + render json: { error: exception.message }, status: :not_implemented + end +end diff --git a/enterprise/app/controllers/enterprise/webhooks/stripe_controller.rb b/enterprise/app/controllers/enterprise/webhooks/stripe_controller.rb index 0cc523889..f3bcf7f4d 100644 --- a/enterprise/app/controllers/enterprise/webhooks/stripe_controller.rb +++ b/enterprise/app/controllers/enterprise/webhooks/stripe_controller.rb @@ -35,6 +35,8 @@ class Enterprise::Webhooks::StripeController < ActionController::API parsed_payload = JSON.parse(payload) event_type = parsed_payload['type'] + return ENV.fetch('STRIPE_WEBHOOK_SECRET', nil) if event_type.blank? + if v2_billing_event?(event_type) ENV.fetch('STRIPE_WEBHOOK_SECRET_V2', nil) else @@ -43,7 +45,9 @@ class Enterprise::Webhooks::StripeController < ActionController::API end def v2_billing_event?(event_type) + return false if event_type.blank? + Rails.logger.debug { "V2 billing event: #{event_type}" } - event_type.start_with?('v2.') if event_type.present? + event_type.start_with?('v2.') end end diff --git a/enterprise/app/services/enterprise/billing/concerns/plan_feature_manager.rb b/enterprise/app/services/enterprise/billing/concerns/plan_feature_manager.rb index 5a5e955fe..a7e971730 100644 --- a/enterprise/app/services/enterprise/billing/concerns/plan_feature_manager.rb +++ b/enterprise/app/services/enterprise/billing/concerns/plan_feature_manager.rb @@ -46,10 +46,7 @@ module Enterprise::Billing::Concerns::PlanFeatureManager end def enable_features_for_current_plan(plan_name) - # First disable all premium features to handle downgrades disable_all_premium_features - - # Then enable features based on the current plan enable_plan_specific_features(plan_name) end @@ -58,7 +55,7 @@ module Enterprise::Billing::Concerns::PlanFeatureManager # Enable features based on plan hierarchy case plan_name - when 'Startup', 'Startups' + when 'Startups' # Startup plan gets the basic features account.enable_features(*STARTUP_PLAN_FEATURES) when 'Business' diff --git a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb index d2cb238ad..68c6b5850 100644 --- a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb +++ b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb @@ -12,7 +12,7 @@ module Enterprise::Billing::Concerns::PlanProvisioningHelper end def extract_plan_name(plan_definition) - plan_definition[:display_name].split.find { |word| %w[Startup Startups Business Enterprise].include?(word) } + plan_definition[:display_name].split.find { |word| %w[Startups Business Enterprise].include?(word) } end def update_account_plan(new_pricing_plan_id, quantity, next_billing_date) 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 f48501efc..2673ff31b 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 @@ -60,6 +60,8 @@ module Enterprise::Billing::Concerns::StripeV2ClientHelper end def extract_attribute(object, key) - object.respond_to?(key) ? object.public_send(key) : object[key.to_s] + return object.public_send(key) if object.respond_to?(key) + + object[key.to_s] end end 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 05f8048cf..26407ffc3 100644 --- a/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb +++ b/enterprise/app/services/enterprise/billing/create_stripe_customer_service.rb @@ -35,7 +35,7 @@ class Enterprise::Billing::CreateStripeCustomerService end def raise_config_error - raise StandardError, 'V2 billing configuration is required. Please configure STRIPE_HACKER_PLAN_ID.' + raise StandardError, I18n.t('errors.enterprise.billing.v2_configuration_required') end def existing_subscription? diff --git a/enterprise/app/services/enterprise/billing/v2/base_service.rb b/enterprise/app/services/enterprise/billing/v2/base_service.rb index 7ab1ccd9c..e3792915e 100644 --- a/enterprise/app/services/enterprise/billing/v2/base_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/base_service.rb @@ -28,7 +28,7 @@ class Enterprise::Billing::V2::BaseService # Update response credits (monthly/topup with auto-calculation of total) def update_response_credits(monthly: nil, topup: nil) # Calculate and update total in limits hash ONLY - return unless monthly || topup + return if monthly.nil? && topup.nil? new_monthly = monthly || response_monthly_credits new_topup = topup || response_topup_credits @@ -44,23 +44,13 @@ class Enterprise::Billing::V2::BaseService def update_limits(updates) return if updates.blank? - current_limits = account.limits.present? ? account.limits.deep_dup : {} - updates.each do |key, value| - current_limits[key.to_s] = value - end - - account.update!(limits: current_limits) + account.update!(limits: (account.limits || {}).merge(updates.transform_keys(&:to_s))) end def update_custom_attributes(updates) return if updates.blank? - current_attributes = account.custom_attributes.present? ? account.custom_attributes.deep_dup : {} - updates.each do |key, value| - current_attributes[key.to_s] = value - end - - account.update!(custom_attributes: current_attributes) + account.update!(custom_attributes: (account.custom_attributes || {}).merge(updates.transform_keys(&:to_s))) end def custom_attribute(key) 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 4e21bf0e5..6b5766927 100644 --- a/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/checkout_session_service.rb @@ -13,13 +13,13 @@ class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2: validate_params store_pending_subscription_quantity session = create_checkout_session(checkout_session_params) - { success: true, redirect_url: session.url } + session.url end private def validate_params - raise StandardError, 'Customer ID required. Please create a Stripe customer first.' if stripe_customer_id.blank? + raise StandardError, I18n.t('errors.enterprise.billing.stripe_customer_required') if stripe_customer_id.blank? end def store_pending_subscription_quantity @@ -50,7 +50,7 @@ class Enterprise::Billing::V2::CheckoutSessionService < Enterprise::Billing::V2: def build_checkout_items lookup_key = extract_license_lookup_key - raise StandardError, "Lookup key not found for pricing plan #{@pricing_plan_id}" unless lookup_key + raise StandardError, I18n.t('errors.enterprise.billing.lookup_key_not_found', pricing_plan_id: @pricing_plan_id) unless lookup_key [ { 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 e4d9a8b39..6ebc5c7ef 100644 --- a/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/credit_management_service.rb @@ -32,7 +32,7 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2 end grants.reject { |grant| grant[:credits].zero? } rescue Stripe::StripeError => e - Rails.logger.error("Failed to fetch credit grants: #{e.message}") + ChatwootExceptionTracker.new(e, account: account).capture_exception [] end diff --git a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb index 12655104e..c45a79f38 100644 --- a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb +++ b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb @@ -13,7 +13,7 @@ module Enterprise::Billing::V2::PlanCatalog }, { key: :startup, - display_name: 'Chatwoot Startup', + display_name: 'Chatwoot Startups', base_fee: 19.0, monthly_credits: 300, config_key: 'STRIPE_STARTUP_PLAN_ID', diff --git a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb index 9bdc106ec..17ba5c5ed 100644 --- a/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/subscription_provisioning_service.rb @@ -49,6 +49,8 @@ class Enterprise::Billing::V2::SubscriptionProvisioningService < Enterprise::Bil def cancel_subscription hacker_plan_config = InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID') + return if hacker_plan_config.nil? + pricing_plan_id = hacker_plan_config.value # Update subscription status and plan details diff --git a/enterprise/app/services/enterprise/billing/v2/webhook_handler_service.rb b/enterprise/app/services/enterprise/billing/v2/webhook_handler_service.rb index ee569b217..bd227df2e 100644 --- a/enterprise/app/services/enterprise/billing/v2/webhook_handler_service.rb +++ b/enterprise/app/services/enterprise/billing/v2/webhook_handler_service.rb @@ -3,9 +3,8 @@ class Enterprise::Billing::V2::WebhookHandlerService def perform(event:) @event = event - return { success: false, message: 'Event is required' } if @event.blank? - - return { success: false, message: 'Account not found' } if account.blank? + raise StandardError, 'Event is required' if @event.blank? + raise StandardError, 'Account not found' if account.blank? case @event.type when 'v2.billing.pricing_plan_subscription.servicing_activated' @@ -14,12 +13,10 @@ class Enterprise::Billing::V2::WebhookHandlerService when 'v2.billing.cadence.billed' Rails.logger.info "Handling cadence billed event: #{@event.related_object.id}" refresh_account_subscription_details(@event.related_object.id) - else - { success: true } end rescue StandardError => e - Rails.logger.error "Error processing V2 webhook: #{e.message}" - { success: false, error: e.message } + ChatwootExceptionTracker.new(e, account: account).capture_exception + raise end private From 69b818896a30863f9af6a50646591a74ac1f4dc8 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Wed, 19 Nov 2025 20:01:49 +0530 Subject: [PATCH 4/5] update plan catalog display_name --- .../concerns/plan_provisioning_helper.rb | 21 +------------------ .../enterprise/billing/v2/plan_catalog.rb | 8 +++---- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb index 68c6b5850..18ad1f1b6 100644 --- a/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb +++ b/enterprise/app/services/enterprise/billing/concerns/plan_provisioning_helper.rb @@ -7,26 +7,7 @@ module Enterprise::Billing::Concerns::PlanProvisioningHelper plan_definition = Enterprise::Billing::V2::PlanCatalog.definition_for(new_pricing_plan_id) return unless plan_definition - plan_name = extract_plan_name(plan_definition) + plan_name = plan_definition[:display_name] enable_plan_specific_features(plan_name) if plan_name.present? end - - def extract_plan_name(plan_definition) - plan_definition[:display_name].split.find { |word| %w[Startups Business Enterprise].include?(word) } - end - - def update_account_plan(new_pricing_plan_id, quantity, next_billing_date) - attributes = { - 'stripe_pricing_plan_id' => new_pricing_plan_id, - 'pending_stripe_pricing_plan_id' => nil, - 'pending_subscription_quantity' => nil, - 'subscribed_quantity' => quantity, - 'next_billing_date' => next_billing_date - } - - plan_definition = Enterprise::Billing::V2::PlanCatalog.definition_for(new_pricing_plan_id) - attributes['plan_name'] = plan_definition[:display_name] if plan_definition - - update_custom_attributes(attributes) - end end diff --git a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb index c45a79f38..8d9d3b0b4 100644 --- a/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb +++ b/enterprise/app/services/enterprise/billing/v2/plan_catalog.rb @@ -5,7 +5,7 @@ module Enterprise::Billing::V2::PlanCatalog PLAN_DEFINITIONS = [ { key: :free, - display_name: 'Chatwoot Hacker', + display_name: 'Hacker', base_fee: 0.0, monthly_credits: 0, config_key: 'STRIPE_HACKER_PLAN_ID', @@ -13,7 +13,7 @@ module Enterprise::Billing::V2::PlanCatalog }, { key: :startup, - display_name: 'Chatwoot Startups', + display_name: 'Startups', base_fee: 19.0, monthly_credits: 300, config_key: 'STRIPE_STARTUP_PLAN_ID', @@ -21,7 +21,7 @@ module Enterprise::Billing::V2::PlanCatalog }, { key: :business, - display_name: 'Chatwoot Business', + display_name: 'Business', base_fee: 39.0, monthly_credits: 500, config_key: 'STRIPE_BUSINESS_PLAN_ID', @@ -29,7 +29,7 @@ module Enterprise::Billing::V2::PlanCatalog }, { key: :enterprise, - display_name: 'Chatwoot Enterprise', + display_name: 'Enterprise', base_fee: 99.0, monthly_credits: 800, config_key: 'STRIPE_ENTERPRISE_PLAN_ID', From 7d2307d34387f803fcfe4b6285c4cc4671548d1d Mon Sep 17 00:00:00 2001 From: Tanmay Sharma Date: Mon, 24 Nov 2025 19:15:01 +0530 Subject: [PATCH 5/5] fix spec handle stripe event service spec --- .../billing/handle_stripe_event_service_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb b/spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb index 51221ac5c..cc4a5fc47 100644 --- a/spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb +++ b/spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb @@ -328,7 +328,7 @@ describe Enterprise::Billing::HandleStripeEventService do context 'when handling monthly credit grant' do it 'adds credits from Stripe' do - allow(credit_service).to receive(:add_response_topup_credits) + allow(credit_service).to receive(:sync_monthly_response_credits) # Webhook event object (minimal, just has ID) grant_event_object = OpenStruct.new( @@ -355,13 +355,13 @@ describe Enterprise::Billing::HandleStripeEventService do stripe_event_service.new.perform(event: event) - expect(credit_service).to have_received(:add_response_topup_credits).with(2000) + expect(credit_service).to have_received(:sync_monthly_response_credits).with(2000) end end context 'when handling topup credit grant' do it 'adds topup credits' do - allow(credit_service).to receive(:add_response_topup_credits) + allow(credit_service).to receive(:sync_monthly_response_credits) # Webhook event object (minimal, just has ID) grant_event_object = OpenStruct.new( @@ -388,7 +388,7 @@ describe Enterprise::Billing::HandleStripeEventService do stripe_event_service.new.perform(event: event) - expect(credit_service).to have_received(:add_response_topup_credits).with(500) + expect(credit_service).to have_received(:sync_monthly_response_credits).with(500) end end