sanity changes

This commit is contained in:
Tanmay Deep Sharma
2025-10-29 03:03:10 +05:30
parent 1689d5cf5c
commit 81c1ab73e6
10 changed files with 96 additions and 95 deletions
+1
View File
@@ -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
View File
@@ -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