sanity changes
This commit is contained in:
@@ -71,6 +71,7 @@ class DashboardController < ActionController::Base
|
||||
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
|
||||
IS_ENTERPRISE: ChatwootApp.enterprise?,
|
||||
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
|
||||
STRIPE_BILLING_V2_ENABLED: GlobalConfigService.load('STRIPE_BILLING_V2_ENABLED', 'false'),
|
||||
GIT_SHA: GIT_HASH
|
||||
}
|
||||
end
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ configs = [
|
||||
meter_event_name: 'chatwoot.usage',
|
||||
plan_display_name: 'Chatwoot Business',
|
||||
plan_lookup_key: 'chatwoot_business_v2',
|
||||
lookup_key: 'chatwoot_business_plan_license_fee_v2',
|
||||
lookup_key: 'chatwoot_business_license_fee_v2',
|
||||
service_action_lookup_key: 'chatwoot_business_plan_v2_credits',
|
||||
metered_item_lookup_key: 'chatwoot_business_plan_v2_usage',
|
||||
licensed_item_display_name: 'Seat',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Enterprise::Billing::CreateStripeCustomerService
|
||||
include Enterprise::Billing::Concerns::PlanFeatureManager
|
||||
|
||||
pattr_initialize [:account!]
|
||||
|
||||
DEFAULT_QUANTITY = 2
|
||||
@@ -7,21 +9,14 @@ class Enterprise::Billing::CreateStripeCustomerService
|
||||
return if existing_subscription?
|
||||
|
||||
customer_id = prepare_customer_id
|
||||
subscription = Stripe::Subscription.create(
|
||||
{
|
||||
customer: customer_id,
|
||||
items: [{ price: price_id, quantity: default_quantity }]
|
||||
}
|
||||
)
|
||||
account.update!(
|
||||
custom_attributes: {
|
||||
stripe_customer_id: customer_id,
|
||||
stripe_price_id: subscription['plan']['id'],
|
||||
stripe_product_id: subscription['plan']['product'],
|
||||
plan_name: default_plan['name'],
|
||||
subscribed_quantity: subscription['quantity']
|
||||
}
|
||||
)
|
||||
|
||||
if billing_v2_enabled?
|
||||
update_account_for_v2_billing(customer_id)
|
||||
enable_plan_specific_features(default_plan['name'])
|
||||
else
|
||||
subscription = create_subscription(customer_id)
|
||||
update_account_with_subscription(customer_id, subscription)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -53,6 +48,10 @@ class Enterprise::Billing::CreateStripeCustomerService
|
||||
price_ids.first
|
||||
end
|
||||
|
||||
def billing_v2_enabled?
|
||||
ENV.fetch('STRIPE_BILLING_V2_ENABLED', 'false') == 'true'
|
||||
end
|
||||
|
||||
def existing_subscription?
|
||||
stripe_customer_id = account.custom_attributes['stripe_customer_id']
|
||||
return false if stripe_customer_id.blank?
|
||||
@@ -66,4 +65,34 @@ class Enterprise::Billing::CreateStripeCustomerService
|
||||
)
|
||||
subscriptions.data.present?
|
||||
end
|
||||
|
||||
def create_subscription(customer_id)
|
||||
Stripe::Subscription.create(
|
||||
customer: customer_id,
|
||||
items: [{ price: price_id, quantity: default_quantity }]
|
||||
)
|
||||
end
|
||||
|
||||
def update_account_for_v2_billing(customer_id)
|
||||
account.update!(
|
||||
custom_attributes: {
|
||||
stripe_customer_id: customer_id,
|
||||
stripe_pricing_plan_id: InstallationConfig.find_by(name: 'STRIPE_HACKER_PLAN_ID').value,
|
||||
plan_name: 'Hacker',
|
||||
subscribed_quantity: DEFAULT_QUANTITY
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def update_account_with_subscription(customer_id, subscription)
|
||||
account.update!(
|
||||
custom_attributes: {
|
||||
stripe_customer_id: customer_id,
|
||||
stripe_price_id: subscription['plan']['id'],
|
||||
stripe_product_id: subscription['plan']['product'],
|
||||
plan_name: default_plan['name'],
|
||||
subscribed_quantity: subscription['quantity']
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,9 +8,6 @@ class Enterprise::Billing::V2::CancelSubscriptionService < Enterprise::Billing::
|
||||
# @return [Hash] { success:, cancel_at_period_end:, period_end:, message: }
|
||||
#
|
||||
def cancel_subscription
|
||||
return { success: false, message: 'Not a V2 billing account' } unless v2_enabled?
|
||||
return { success: false, message: 'No active subscription' } unless active_subscription?
|
||||
|
||||
with_locked_account do
|
||||
billing_intent = create_deactivate_intent
|
||||
reserve_billing_intent(billing_intent)
|
||||
@@ -55,6 +52,7 @@ class Enterprise::Billing::V2::CancelSubscriptionService < Enterprise::Billing::
|
||||
|
||||
def store_next_billing_date(cadence_id)
|
||||
cadence = retrieve_billing_cadence(cadence_id)
|
||||
Rails.logger.info("Cadence: #{cadence}")
|
||||
@next_billing_date = extract_attribute(cadence, :next_billing_date)
|
||||
end
|
||||
|
||||
@@ -126,14 +124,6 @@ class Enterprise::Billing::V2::CancelSubscriptionService < Enterprise::Billing::
|
||||
}
|
||||
end
|
||||
|
||||
def v2_enabled?
|
||||
custom_attribute('stripe_billing_version')&.to_i == 2
|
||||
end
|
||||
|
||||
def active_subscription?
|
||||
custom_attribute('subscription_status') == 'active'
|
||||
end
|
||||
|
||||
def stripe_api_options
|
||||
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||
end
|
||||
|
||||
@@ -9,8 +9,6 @@ class Enterprise::Billing::V2::ChangePlanService < Enterprise::Billing::V2::Base
|
||||
# @return [Hash] { success:, message: }
|
||||
#
|
||||
def change_plan(new_pricing_plan_id:, quantity: 1)
|
||||
return { success: false, message: 'Not a V2 billing account' } unless v2_enabled?
|
||||
return { success: false, message: 'No active subscription' } unless active_subscription?
|
||||
return { success: false, message: 'Invalid quantity' } unless quantity.positive?
|
||||
|
||||
with_locked_account do
|
||||
@@ -179,14 +177,6 @@ class Enterprise::Billing::V2::ChangePlanService < Enterprise::Billing::V2::Base
|
||||
}
|
||||
end
|
||||
|
||||
def v2_enabled?
|
||||
custom_attribute('stripe_billing_version')&.to_i == 2
|
||||
end
|
||||
|
||||
def active_subscription?
|
||||
custom_attribute('subscription_status') == 'active'
|
||||
end
|
||||
|
||||
def stripe_api_options
|
||||
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||
end
|
||||
|
||||
@@ -99,9 +99,10 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
||||
stripe_api_options
|
||||
)
|
||||
|
||||
response.data.map do |grant|
|
||||
grants = response.data.map do |grant|
|
||||
transform_credit_grant(grant)
|
||||
end
|
||||
grants.reject { |grant| grant[:credits].zero? }
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.error("Failed to fetch credit grants: #{e.message}")
|
||||
[]
|
||||
@@ -132,36 +133,32 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
||||
|
||||
private
|
||||
|
||||
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
||||
def transform_credit_grant(grant)
|
||||
# Stripe objects have symbol keys when converted to hash, or access via methods
|
||||
# Use the grant object directly for most reliable access
|
||||
category = grant[:category] || grant.category
|
||||
metadata = grant[:metadata] || grant.metadata || {}
|
||||
|
||||
# Determine credits amount based on grant type
|
||||
credits = if category == 'paid' && metadata['credits']
|
||||
metadata['credits'].to_i
|
||||
elsif category == 'promotional'
|
||||
grant_id = grant[:id] || grant.id
|
||||
fetch_monthly_credit_amount(grant_id)
|
||||
else
|
||||
0
|
||||
end
|
||||
category = grant_attribute(grant, :category)
|
||||
metadata = grant_attribute(grant, :metadata) || {}
|
||||
|
||||
{
|
||||
id: grant[:id] || grant.id,
|
||||
name: grant[:name] || grant.name,
|
||||
credits: credits,
|
||||
id: grant_attribute(grant, :id),
|
||||
name: grant_attribute(grant, :name),
|
||||
credits: calculate_grant_credits(category, metadata),
|
||||
category: category,
|
||||
source: metadata['source'] || category,
|
||||
effective_at: parse_timestamp(grant[:effective_at] || grant.effective_at),
|
||||
expires_at: parse_timestamp(grant[:expires_at] || grant.expires_at),
|
||||
voided_at: parse_timestamp(grant[:voided_at] || grant.voided_at),
|
||||
created_at: parse_timestamp(grant[:created] || grant.created)
|
||||
effective_at: parse_timestamp(grant_attribute(grant, :effective_at)),
|
||||
expires_at: parse_timestamp(grant_attribute(grant, :expires_at)),
|
||||
voided_at: parse_timestamp(grant_attribute(grant, :voided_at)),
|
||||
created_at: parse_timestamp(grant_attribute(grant, :created))
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
||||
|
||||
def grant_attribute(grant, key)
|
||||
grant[key] || grant.public_send(key)
|
||||
end
|
||||
|
||||
def calculate_grant_credits(category, metadata)
|
||||
return metadata['credits'].to_i if category == 'paid' && metadata['credits']
|
||||
|
||||
0
|
||||
end
|
||||
|
||||
def parse_timestamp(timestamp)
|
||||
return nil unless timestamp
|
||||
@@ -169,20 +166,6 @@ class Enterprise::Billing::V2::CreditManagementService < Enterprise::Billing::V2
|
||||
Time.zone.at(timestamp)
|
||||
end
|
||||
|
||||
def fetch_monthly_credit_amount(grant_id)
|
||||
# Try to get from credit transactions with matching grant_id in metadata
|
||||
transaction = account.credit_transactions
|
||||
.where(transaction_type: 'grant', credit_type: 'monthly')
|
||||
.where("metadata->>'grant_id' = ?", grant_id)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
|
||||
return transaction.amount if transaction
|
||||
|
||||
# Fallback: return the current monthly_credits from account
|
||||
monthly_credits
|
||||
end
|
||||
|
||||
def stripe_customer_id
|
||||
custom_attribute('stripe_customer_id')
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ module Enterprise::Billing::V2::PlanCatalog
|
||||
base_fee: 39.0,
|
||||
monthly_credits: 50_000,
|
||||
config_key: 'STRIPE_BUSINESS_PLAN_ID',
|
||||
licensed_item_lookup_key: 'chatwoot_business_plan_license_fee_v2'
|
||||
licensed_item_lookup_key: 'chatwoot_business_license_fee_v2'
|
||||
},
|
||||
{
|
||||
key: :enterprise,
|
||||
|
||||
@@ -42,14 +42,14 @@ class Enterprise::Billing::V2::PricingPlanService < Enterprise::Billing::V2::Bas
|
||||
plan = create_pricing_plan(display_name: config[:plan_display_name], lookup_key: config[:plan_lookup_key])
|
||||
|
||||
builder = component_builder
|
||||
builder.add_license_fee_component(plan, config)
|
||||
service_action = builder.add_service_action_component(plan, config, cpu)
|
||||
rate_card = builder.add_rate_card_component(plan, config, meter, cpu)
|
||||
builder.add_license_fee_component(plan, config) if config[:license_fee_amount].to_i.positive?
|
||||
builder.add_service_action_component(plan, config, cpu) if config[:monthly_credit_amount].to_i.positive?
|
||||
builder.add_rate_card_component(plan, config, meter, cpu)
|
||||
|
||||
# Make the latest version live
|
||||
make_plan_version_live(plan.id)
|
||||
|
||||
build_plan_result(plan, cpu, meter, rate_card, service_action)
|
||||
build_plan_result(plan, cpu, meter)
|
||||
end
|
||||
|
||||
def get_or_create_cpu(config)
|
||||
@@ -111,14 +111,12 @@ class Enterprise::Billing::V2::PricingPlanService < Enterprise::Billing::V2::Bas
|
||||
)
|
||||
end
|
||||
|
||||
def build_plan_result(plan, cpu, meter, rate_card, service_action)
|
||||
def build_plan_result(plan, cpu, meter)
|
||||
{
|
||||
success: true,
|
||||
pricing_plan: plan,
|
||||
custom_pricing_unit: cpu,
|
||||
meter: meter,
|
||||
rate_card: rate_card,
|
||||
service_action: service_action
|
||||
meter: meter
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,18 +19,38 @@ class Enterprise::Billing::V2::TopupService < Enterprise::Billing::V2::BaseServi
|
||||
def validate_topup_request(credits)
|
||||
return { valid: false, success: false, message: 'Invalid topup amount' } unless credits.to_i.positive?
|
||||
|
||||
# Check if account has a valid subscription plan
|
||||
plan_validation = validate_subscription_plan
|
||||
return plan_validation unless plan_validation[:valid]
|
||||
|
||||
topup_definition = Enterprise::Billing::V2::TopupCatalog.find_option(credits)
|
||||
return { valid: false, success: false, message: 'Unsupported topup amount' } unless topup_definition
|
||||
return { valid: false, success: false, message: 'Stripe customer not configured' } if stripe_customer_id.blank?
|
||||
|
||||
# Check if customer has a default payment method
|
||||
unless customer_has_payment_method?
|
||||
return { valid: false, success: false, message: 'No payment method found. Please add a payment method before making a purchase.' }
|
||||
return { valid: false, success: false,
|
||||
message: 'No default payment method found. Please add a default payment method before making a purchase.' }
|
||||
end
|
||||
|
||||
{ valid: true, topup_definition: topup_definition }
|
||||
end
|
||||
|
||||
def validate_subscription_plan
|
||||
plan_name = custom_attribute('plan_name')
|
||||
|
||||
# Block topup if no plan or on Hacker plan
|
||||
if plan_name.blank? || plan_name.downcase == 'hacker'
|
||||
return {
|
||||
valid: false,
|
||||
success: false,
|
||||
message: 'Top-ups are only available for Startup, Business, and Enterprise plans. Please upgrade your plan to purchase credits.'
|
||||
}
|
||||
end
|
||||
|
||||
{ valid: true }
|
||||
end
|
||||
|
||||
def customer_has_payment_method?
|
||||
customer = Stripe::Customer.retrieve(stripe_customer_id, stripe_api_options)
|
||||
# Check if customer has a default payment method or any payment methods attached
|
||||
|
||||
@@ -8,8 +8,6 @@ class Enterprise::Billing::V2::UpdateSubscriptionService < Enterprise::Billing::
|
||||
# @return [Hash] { success:, message: }
|
||||
#
|
||||
def update_quantity(quantity:)
|
||||
return { success: false, message: 'Not a V2 billing account' } unless v2_enabled?
|
||||
return { success: false, message: 'No active subscription' } unless active_subscription?
|
||||
return { success: false, message: 'Invalid quantity' } unless quantity.positive?
|
||||
|
||||
with_locked_account do
|
||||
@@ -129,14 +127,6 @@ class Enterprise::Billing::V2::UpdateSubscriptionService < Enterprise::Billing::
|
||||
}
|
||||
end
|
||||
|
||||
def v2_enabled?
|
||||
custom_attribute('stripe_billing_version')&.to_i == 2
|
||||
end
|
||||
|
||||
def active_subscription?
|
||||
custom_attribute('subscription_status') == 'active'
|
||||
end
|
||||
|
||||
def stripe_api_options
|
||||
{ api_key: ENV.fetch('STRIPE_SECRET_KEY', nil), stripe_version: '2025-08-27.preview' }
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user