fix: settle captain credits on subscription cancellation (#14089)
## Linear Ticket - https://linear.app/chatwoot/issue/CW-6875/captain-credits-3-bugs-in-stripe-subscription-lifecycle-cancel-ratchet ## Description Fixes Captain credit settlement on subscription cancellation. Previously `limits['captain_responses']` and `captain_responses_usage` were left in their pre-cancellation state, which caused incorrect credit totals when a customer re-subscribed. Cancellation now settles the monthly allotment (preserving any remaining topup) and resets the usage counter. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? 1. Set up an account subscribed to a paid plan (e.g. Startups) so `limits['captain_responses']` reflects the plan allotment. 2. Fire `customer.subscription.deleted` for that account's Stripe customer. Confirm the limits. 3. Fire `customer.subscription.updated` re-subscribing to the paid plan. Confirm the limits. 4. Repeat cancel → re-subscribe several times; ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -4,15 +4,17 @@ class Enterprise::Billing::CreateStripeCustomerService
|
||||
DEFAULT_QUANTITY = 2
|
||||
|
||||
def perform
|
||||
return if existing_subscription?
|
||||
active_sub = active_subscription
|
||||
return false if active_sub && !default_plan_subscription?(active_sub)
|
||||
|
||||
customer_id = prepare_customer_id
|
||||
subscription = Stripe::Subscription.create(customer: customer_id, items: [{ price: price_id, quantity: default_quantity }])
|
||||
subscription = active_sub || Stripe::Subscription.create(customer: customer_id, items: [{ price: price_id, quantity: default_quantity }])
|
||||
custom_attributes = build_custom_attributes(customer_id, subscription)
|
||||
custom_attributes.except!('is_creating_customer')
|
||||
|
||||
account.update!(custom_attributes: custom_attributes)
|
||||
Enterprise::Billing::ReconcilePlanFeaturesService.new(account: account).perform
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
@@ -44,18 +46,21 @@ class Enterprise::Billing::CreateStripeCustomerService
|
||||
price_ids.first
|
||||
end
|
||||
|
||||
def existing_subscription?
|
||||
def active_subscription
|
||||
stripe_customer_id = account.custom_attributes['stripe_customer_id']
|
||||
return false if stripe_customer_id.blank?
|
||||
return nil if stripe_customer_id.blank?
|
||||
|
||||
subscriptions = Stripe::Subscription.list(
|
||||
Stripe::Subscription.list(
|
||||
{
|
||||
customer: stripe_customer_id,
|
||||
status: 'active',
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
subscriptions.data.present?
|
||||
).data.first
|
||||
end
|
||||
|
||||
def default_plan_subscription?(subscription)
|
||||
default_plan['price_ids'].include?(subscription['plan']['id'])
|
||||
end
|
||||
|
||||
def build_custom_attributes(customer_id, subscription)
|
||||
|
||||
@@ -47,9 +47,8 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
|
||||
def current_plan_credits
|
||||
plan_name = account.custom_attributes['plan_name']
|
||||
return { responses: 0, documents: 0 } if plan_name.blank?
|
||||
|
||||
get_plan_credits(plan_name)
|
||||
plan_credits = get_plan_credits(plan_name) if plan_name.present?
|
||||
plan_credits || { responses: 0, documents: 0 }
|
||||
end
|
||||
|
||||
def update_account_attributes(subscription, plan)
|
||||
@@ -71,19 +70,28 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
# skipping self hosted plan events
|
||||
return if account.blank?
|
||||
|
||||
Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||
previous_monthly_credits = current_plan_credits[:responses]
|
||||
return unless Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||
|
||||
account.with_lock do
|
||||
previous_usage = { responses: account.custom_attributes['captain_responses_usage'].to_i, monthly: previous_monthly_credits }
|
||||
adjust_captain_credits(previous_usage, new_plan_credits: 0)
|
||||
account.reset_response_usage
|
||||
end
|
||||
end
|
||||
|
||||
def handle_subscription_credits(plan, previous_usage)
|
||||
current_limits = account.limits || {}
|
||||
adjust_captain_credits(previous_usage, new_plan_credits: get_plan_credits(plan['name'])[:responses])
|
||||
end
|
||||
|
||||
def adjust_captain_credits(previous_usage, new_plan_credits:)
|
||||
current_limits = account.limits || {}
|
||||
current_credits = current_limits['captain_responses'].to_i
|
||||
new_plan_credits = get_plan_credits(plan['name'])[:responses]
|
||||
|
||||
consumed_topup_credits = [previous_usage[:responses] - previous_usage[:monthly], 0].max
|
||||
updated_credits = current_credits - consumed_topup_credits - previous_usage[:monthly] + new_plan_credits
|
||||
updated_credits = [current_credits - consumed_topup_credits - previous_usage[:monthly] + new_plan_credits, 0].max
|
||||
|
||||
Rails.logger.info("Updating subscription credits for account #{account.id}: #{current_credits} -> #{updated_credits}")
|
||||
Rails.logger.info("Updating captain credits for account #{account.id}: #{current_credits} -> #{updated_credits}")
|
||||
account.update!(limits: current_limits.merge('captain_responses' => updated_credits))
|
||||
end
|
||||
|
||||
|
||||
@@ -145,10 +145,10 @@ describe Enterprise::Billing::CreateStripeCustomerService do
|
||||
account.update!(custom_attributes: { stripe_customer_id: stripe_customer_id })
|
||||
end
|
||||
|
||||
context 'when customer has active subscriptions' do
|
||||
context 'when customer has an active non-default subscription' do
|
||||
before do
|
||||
allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_list)
|
||||
allow(subscriptions_list).to receive(:data).and_return(['subscription'])
|
||||
allow(subscriptions_list).to receive(:data).and_return([{ 'plan' => { 'id' => 'price_paid_plan' } }])
|
||||
allow(Stripe::Subscription).to receive(:create)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user