fix comments
This commit is contained in:
@@ -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
|
||||
|
||||
+11
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
[
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user