feat(cloud-billing): cancel subscriptions at period end on deletion mark (#13580)

## How to reproduce
In Chatwoot Cloud, mark an account for deletion from account settings
while the account has an active Stripe subscription. Before this change,
deletion marking did not explicitly mark subscriptions to stop renewing
at period end.

## What changed
This PR adds `Enterprise::Billing::CancelCloudSubscriptionsService` and
calls it from the delete action path in
`Enterprise::Api::V1::AccountsController`. The service lists only active
Stripe subscriptions for the customer and sets `cancel_at_period_end:
true` when needed. The account deletion schedule remains unchanged
(existing static 7-day behavior), and Stripe deleted-event fallback
behavior remains unchanged.

## How this was tested
Added and updated specs:
-
`spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
-
`spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb`

Executed:
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
- `bundle exec rspec
spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb:363`
This commit is contained in:
Sojan Jose
2026-02-18 22:10:06 -08:00
committed by GitHub
parent 594333a183
commit 2ab117e8eb
4 changed files with 106 additions and 1 deletions

View File

@@ -0,0 +1,51 @@
require 'rails_helper'
RSpec.describe Enterprise::Billing::CancelCloudSubscriptionsService do
subject(:service) { described_class.new(account: account) }
let(:account) { create(:account, custom_attributes: custom_attributes) }
let(:custom_attributes) { { 'stripe_customer_id' => 'cus_123' } }
describe '#perform' do
context 'when deployment is not cloud' do
it 'does not call stripe subscriptions api' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
allow(Stripe::Subscription).to receive(:list)
service.perform
expect(Stripe::Subscription).not_to have_received(:list)
end
end
context 'when stripe customer id is missing' do
let(:custom_attributes) { {} }
it 'does not call stripe subscriptions api' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
allow(Stripe::Subscription).to receive(:list)
service.perform
expect(Stripe::Subscription).not_to have_received(:list)
end
end
context 'when account is cloud with active subscriptions' do
let(:subscription_response) { Struct.new(:data).new([sub_1, sub_2]) }
let(:sub_1) { instance_double(Stripe::Subscription, id: 'sub_1', cancel_at_period_end: false) }
let(:sub_2) { instance_double(Stripe::Subscription, id: 'sub_2', cancel_at_period_end: true) }
it 'marks only active subscriptions that are not yet set to cancel at period end' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
allow(Stripe::Subscription).to receive(:list).and_return(subscription_response)
allow(Stripe::Subscription).to receive(:update)
service.perform
expect(Stripe::Subscription).to have_received(:list).with(customer: 'cus_123', status: 'active', limit: 100)
expect(Stripe::Subscription).to have_received(:update).with('sub_1', cancel_at_period_end: true).once
end
end
end
end