From 51b39cb682ab55ecbf57bb937054f08779d65b6b Mon Sep 17 00:00:00 2001 From: Prakhar Pratyush Date: Wed, 22 Nov 2023 17:14:02 +0530 Subject: [PATCH] stripe: Add 'do_update_plan' method to the 'BillingSession' class. This commit moves a major portion of the 'update_plan` view to a new shared 'BillingSession.do_update_plan' method. This refactoring will help in minimizing duplicate code while supporting both realm and remote_server customers. --- analytics/tests/test_support_views.py | 14 ++- analytics/views/support.py | 11 +- corporate/lib/stripe.py | 160 ++++++++++++++++++++++---- corporate/tests/test_stripe.py | 44 +++---- corporate/views/billing_page.py | 114 ++---------------- zerver/actions/realm_settings.py | 8 +- 6 files changed, 187 insertions(+), 164 deletions(-) diff --git a/analytics/tests/test_support_views.py b/analytics/tests/test_support_views.py index 4f2cea6a9d..0f20682c5e 100644 --- a/analytics/tests/test_support_views.py +++ b/analytics/tests/test_support_views.py @@ -701,7 +701,9 @@ class TestSupportEndpoint(ZulipTestCase): iago = self.example_user("iago") self.login_user(iago) - with mock.patch("analytics.views.support.downgrade_at_the_end_of_billing_cycle") as m: + with mock.patch( + "analytics.views.support.RealmBillingSession.downgrade_at_the_end_of_billing_cycle" + ) as m: result = self.client_post( "/activity/support", { @@ -709,13 +711,13 @@ class TestSupportEndpoint(ZulipTestCase): "modify_plan": "downgrade_at_billing_cycle_end", }, ) - m.assert_called_once_with(get_realm("zulip")) + m.assert_called_once() self.assert_in_success_response( ["zulip marked for downgrade at the end of billing cycle"], result ) with mock.patch( - "analytics.views.support.downgrade_now_without_creating_additional_invoices" + "analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices" ) as m: result = self.client_post( "/activity/support", @@ -724,13 +726,13 @@ class TestSupportEndpoint(ZulipTestCase): "modify_plan": "downgrade_now_without_additional_licenses", }, ) - m.assert_called_once_with(get_realm("zulip")) + m.assert_called_once() self.assert_in_success_response( ["zulip downgraded without creating additional invoices"], result ) with mock.patch( - "analytics.views.support.downgrade_now_without_creating_additional_invoices" + "analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices" ) as m1: with mock.patch("analytics.views.support.void_all_open_invoices", return_value=1) as m2: result = self.client_post( @@ -740,7 +742,7 @@ class TestSupportEndpoint(ZulipTestCase): "modify_plan": "downgrade_now_void_open_invoices", }, ) - m1.assert_called_once_with(get_realm("zulip")) + m1.assert_called_once() m2.assert_called_once_with(get_realm("zulip")) self.assert_in_success_response( ["zulip downgraded and voided 1 open invoices"], result diff --git a/analytics/views/support.py b/analytics/views/support.py index 7fbed8b8f9..43d29aea51 100644 --- a/analytics/views/support.py +++ b/analytics/views/support.py @@ -55,8 +55,6 @@ if settings.ZILENCER_ENABLED: if settings.BILLING_ENABLED: from corporate.lib.stripe import ( RealmBillingSession, - downgrade_at_the_end_of_billing_cycle, - downgrade_now_without_creating_additional_invoices, get_latest_seat_count, switch_realm_from_standard_to_plus_plan, void_all_open_invoices, @@ -264,18 +262,21 @@ def support( approve_realm_sponsorship(realm, acting_user=acting_user) context["success_message"] = f"Sponsorship approved for {realm.string_id}" elif modify_plan is not None: + billing_session = RealmBillingSession( + user=acting_user, realm=realm, support_session=True + ) if modify_plan == "downgrade_at_billing_cycle_end": - downgrade_at_the_end_of_billing_cycle(realm) + billing_session.downgrade_at_the_end_of_billing_cycle() context[ "success_message" ] = f"{realm.string_id} marked for downgrade at the end of billing cycle" elif modify_plan == "downgrade_now_without_additional_licenses": - downgrade_now_without_creating_additional_invoices(realm) + billing_session.downgrade_now_without_creating_additional_invoices() context[ "success_message" ] = f"{realm.string_id} downgraded without creating additional invoices" elif modify_plan == "downgrade_now_void_open_invoices": - downgrade_now_without_creating_additional_invoices(realm) + billing_session.downgrade_now_without_creating_additional_invoices() voided_invoices_count = void_all_open_invoices(realm) context[ "success_message" diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index c5db97ae18..e487ec451e 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -451,6 +451,13 @@ class InitialUpgradeRequest: tier: int +@dataclass +class UpdatePlanRequest: + status: Optional[int] + licenses: Optional[int] + licenses_at_next_renewal: Optional[int] + + class AuditLogEventType(Enum): STRIPE_CUSTOMER_CREATED = 1 STRIPE_CARD_CHANGED = 2 @@ -1264,6 +1271,134 @@ class BillingSession(ABC): return None, context + def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None: + if plan is None: # nocoverage + # TODO: Add test coverage. Right now, this logic is used + # in production but mocked in tests. + customer = self.get_customer() + assert customer is not None + plan = get_current_plan_by_customer(customer) + assert plan is not None + do_change_plan_status(plan, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) + + # During realm deactivation we instantly downgrade the plan to Limited. + # Extra users added in the final month are not charged. Also used + # for the cancellation of Free Trial. + def downgrade_now_without_creating_additional_invoices( + self, + plan: Optional[CustomerPlan] = None, + ) -> None: + if plan is None: + customer = self.get_customer() + if customer is None: + return + plan = get_current_plan_by_customer(customer) + if plan is None: + return # nocoverage + + self.process_downgrade(plan) + plan.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last() + plan.next_invoice_date = next_invoice_date(plan) + plan.save(update_fields=["invoiced_through", "next_invoice_date"]) + + def do_update_plan(self, update_plan_request: UpdatePlanRequest) -> None: + customer = self.get_customer() + assert customer is not None + plan = get_current_plan_by_customer(customer) + assert plan is not None # for mypy + + new_plan, last_ledger_entry = self.make_end_of_cycle_updates_if_needed(plan, timezone_now()) + if new_plan is not None: + raise JsonableError( + _( + "Unable to update the plan. The plan has been expired and replaced with a new plan." + ) + ) + + if last_ledger_entry is None: + raise JsonableError(_("Unable to update the plan. The plan has ended.")) + + status = update_plan_request.status + if status is not None: + if status == CustomerPlan.ACTIVE: + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + do_change_plan_status(plan, status) + elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + self.downgrade_at_the_end_of_billing_cycle(plan=plan) + elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: + assert plan.billing_schedule == CustomerPlan.MONTHLY + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + # Customer needs to switch to an active plan first to avoid unexpected behavior. + assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE + assert plan.fixed_price is None + do_change_plan_status(plan, status) + elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: + assert plan.billing_schedule == CustomerPlan.ANNUAL + assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD + # Customer needs to switch to an active plan first to avoid unexpected behavior. + assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE + assert plan.fixed_price is None + do_change_plan_status(plan, status) + elif status == CustomerPlan.ENDED: + assert plan.is_free_trial() + self.downgrade_now_without_creating_additional_invoices(plan=plan) + return + + licenses = update_plan_request.licenses + if licenses is not None: + if plan.automanage_licenses: + raise JsonableError( + _( + "Unable to update licenses manually. Your plan is on automatic license management." + ) + ) + if last_ledger_entry.licenses == licenses: + raise JsonableError( + _( + "Your plan is already on {licenses} licenses in the current billing period." + ).format(licenses=licenses) + ) + if last_ledger_entry.licenses > licenses: + raise JsonableError( + _("You cannot decrease the licenses in the current billing period.") + ) + validate_licenses( + plan.charge_automatically, + licenses, + self.current_count_for_billed_licenses(), + plan.customer.exempt_from_license_number_check, + ) + update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses) + return + + licenses_at_next_renewal = update_plan_request.licenses_at_next_renewal + if licenses_at_next_renewal is not None: + if plan.automanage_licenses: + raise JsonableError( + _( + "Unable to update licenses manually. Your plan is on automatic license management." + ) + ) + if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal: + raise JsonableError( + _( + "Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses." + ).format(licenses_at_next_renewal=licenses_at_next_renewal) + ) + validate_licenses( + plan.charge_automatically, + licenses_at_next_renewal, + self.current_count_for_billed_licenses(), + plan.customer.exempt_from_license_number_check, + ) + update_license_ledger_for_manual_plan( + plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal + ) + return + + raise JsonableError(_("Nothing to change.")) + class RealmBillingSession(BillingSession): def __init__( @@ -2174,27 +2309,6 @@ def do_change_plan_status(plan: CustomerPlan, status: int) -> None: ) -# During realm deactivation we instantly downgrade the plan to Limited. -# Extra users added in the final month are not charged. Also used -# for the cancellation of Free Trial. -def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None: - plan = get_current_plan_by_realm(realm) - if plan is None: - return - - billing_session = RealmBillingSession(user=None, realm=realm) - billing_session.process_downgrade(plan) - plan.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last() - plan.next_invoice_date = next_invoice_date(plan) - plan.save(update_fields=["invoiced_through", "next_invoice_date"]) - - -def downgrade_at_the_end_of_billing_cycle(realm: Realm) -> None: - plan = get_current_plan_by_realm(realm) - assert plan is not None - do_change_plan_status(plan, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) - - def get_all_invoices_for_customer(customer: Customer) -> Generator[stripe.Invoice, None, None]: if customer.stripe_customer_id is None: return @@ -2251,8 +2365,8 @@ def downgrade_small_realms_behind_on_payments_as_needed() -> None: continue # We've now decided to downgrade this customer and void all invoices, and the below will execute this. - - downgrade_now_without_creating_additional_invoices(realm) + billing_session = RealmBillingSession(user=None, realm=realm) + billing_session.downgrade_now_without_creating_additional_invoices() void_all_open_invoices(realm) context: Dict[str, Union[str, Realm]] = { "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}", diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 6dfab01b71..9b525f65a2 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -2185,7 +2185,7 @@ class StripeTest(StripeTestCase): self.assertEqual(plan.licenses(), self.seat_count) self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} ) @@ -2299,7 +2299,7 @@ class StripeTest(StripeTestCase): assert new_plan is not None with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}, @@ -2488,7 +2488,7 @@ class StripeTest(StripeTestCase): new_plan = get_current_plan_by_realm(user.realm) assert new_plan is not None with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}, @@ -2602,7 +2602,7 @@ class StripeTest(StripeTestCase): assert new_plan is not None with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}, @@ -2767,7 +2767,7 @@ class StripeTest(StripeTestCase): with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} ) @@ -2781,7 +2781,7 @@ class StripeTest(StripeTestCase): assert plan is not None self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch("/json/billing/plan", {"status": CustomerPlan.ACTIVE}) expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.ACTIVE}" self.assertEqual(m.output[0], expected_log) @@ -2807,7 +2807,7 @@ class StripeTest(StripeTestCase): stripe_customer_id = Customer.objects.get(realm=user.realm).id new_plan = get_current_plan_by_realm(user.realm) assert new_plan is not None - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.client_patch( "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} ) @@ -2848,7 +2848,7 @@ class StripeTest(StripeTestCase): self.login_user(user) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.client_patch("/json/billing/plan", {"status": CustomerPlan.ENDED}) plan.refresh_from_db() @@ -2880,7 +2880,7 @@ class StripeTest(StripeTestCase): self.login_user(user) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.client_patch( "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} ) @@ -2935,38 +2935,38 @@ class StripeTest(StripeTestCase): with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.upgrade(invoice=True, licenses=100) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses": 100}) self.assert_json_error_contains( result, "Your plan is already on 100 licenses in the current billing period." ) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100}) self.assert_json_error_contains( result, "Your plan is already scheduled to renew with 100 licenses." ) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses": 50}) self.assert_json_error_contains( result, "You cannot decrease the licenses in the current billing period." ) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 25}) self.assert_json_error_contains( result, "You must purchase licenses for all active users in your organization (minimum 30).", ) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses": 2000}) self.assert_json_error_contains( result, "Invoices with more than 1000 licenses can't be processed from this page." ) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses": 150}) self.assert_json_success(result) invoice_plans_as_needed(self.next_year) @@ -3016,7 +3016,7 @@ class StripeTest(StripeTestCase): for key, value in line_item_params.items(): self.assertEqual(extra_license_item.get(key), value) - with patch("corporate.views.billing_page.timezone_now", return_value=self.next_year): + with patch("corporate.lib.stripe.timezone_now", return_value=self.next_year): result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 120}) self.assert_json_success(result) invoice_plans_as_needed(self.next_year + timedelta(days=365)) @@ -3069,7 +3069,7 @@ class StripeTest(StripeTestCase): with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.local_upgrade(100, False, CustomerPlan.ANNUAL, True, False) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch( "/json/billing/plan", {"licenses_at_next_renewal": get_latest_seat_count(user.realm) - 2}, @@ -3115,11 +3115,11 @@ class StripeTest(StripeTestCase): with patch("corporate.lib.stripe.timezone_now", return_value=self.now): self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses": 100}) self.assert_json_error_contains(result, "Your plan is on automatic license management.") - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch("/json/billing/plan", {"licenses_at_next_renewal": 100}) self.assert_json_error_contains(result, "Your plan is on automatic license management.") @@ -3139,7 +3139,7 @@ class StripeTest(StripeTestCase): self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False) self.login_user(self.example_user("hamlet")) - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): response = self.client_patch("/json/billing/plan", {}) self.assert_json_error_contains(response, "Nothing to change") @@ -3149,7 +3149,7 @@ class StripeTest(StripeTestCase): self.login_user(self.example_user("hamlet")) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE} ) @@ -3171,7 +3171,7 @@ class StripeTest(StripeTestCase): self.login_user(self.example_user("hamlet")) with self.assertLogs("corporate.stripe", "INFO") as m: - with patch("corporate.views.billing_page.timezone_now", return_value=self.now): + with patch("corporate.lib.stripe.timezone_now", return_value=self.now): result = self.client_patch( "/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE} ) diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 4969854f75..e5d8cc5052 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -4,21 +4,10 @@ from typing import Any, Dict, Optional from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse -from django.utils.timezone import now as timezone_now -from django.utils.translation import gettext as _ -from corporate.lib.stripe import ( - RealmBillingSession, - do_change_plan_status, - downgrade_at_the_end_of_billing_cycle, - downgrade_now_without_creating_additional_invoices, - get_latest_seat_count, - update_license_ledger_for_manual_plan, - validate_licenses, -) -from corporate.models import CustomerPlan, get_current_plan_by_realm, get_customer_by_realm +from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest +from corporate.models import CustomerPlan, get_customer_by_realm from zerver.decorator import require_billing_access, zulip_login_required -from zerver.lib.exceptions import JsonableError from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.validator import check_int, check_int_in, check_string @@ -142,96 +131,11 @@ def update_plan( "licenses_at_next_renewal", json_validator=check_int, default=None ), ) -> HttpResponse: - plan = get_current_plan_by_realm(user.realm) - assert plan is not None # for mypy - - realm = plan.customer.realm - billing_session = RealmBillingSession(user=None, realm=realm) - new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed( - plan, timezone_now() + update_plan_request = UpdatePlanRequest( + status=status, + licenses=licenses, + licenses_at_next_renewal=licenses_at_next_renewal, ) - if new_plan is not None: - raise JsonableError( - _("Unable to update the plan. The plan has been expired and replaced with a new plan.") - ) - - if last_ledger_entry is None: - raise JsonableError(_("Unable to update the plan. The plan has ended.")) - - if status is not None: - if status == CustomerPlan.ACTIVE: - assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD - do_change_plan_status(plan, status) - elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE: - assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD - downgrade_at_the_end_of_billing_cycle(user.realm) - elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE: - assert plan.billing_schedule == CustomerPlan.MONTHLY - assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD - # Customer needs to switch to an active plan first to avoid unexpected behavior. - assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE - assert plan.fixed_price is None - do_change_plan_status(plan, status) - elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE: - assert plan.billing_schedule == CustomerPlan.ANNUAL - assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD - # Customer needs to switch to an active plan first to avoid unexpected behavior. - assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE - assert plan.fixed_price is None - do_change_plan_status(plan, status) - elif status == CustomerPlan.ENDED: - assert plan.is_free_trial() - downgrade_now_without_creating_additional_invoices(user.realm) - return json_success(request) - - if licenses is not None: - if plan.automanage_licenses: - raise JsonableError( - _( - "Unable to update licenses manually. Your plan is on automatic license management." - ) - ) - if last_ledger_entry.licenses == licenses: - raise JsonableError( - _( - "Your plan is already on {licenses} licenses in the current billing period." - ).format(licenses=licenses) - ) - if last_ledger_entry.licenses > licenses: - raise JsonableError( - _("You cannot decrease the licenses in the current billing period.") - ) - validate_licenses( - plan.charge_automatically, - licenses, - get_latest_seat_count(user.realm), - plan.customer.exempt_from_license_number_check, - ) - update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses) - return json_success(request) - - if licenses_at_next_renewal is not None: - if plan.automanage_licenses: - raise JsonableError( - _( - "Unable to update licenses manually. Your plan is on automatic license management." - ) - ) - if last_ledger_entry.licenses_at_next_renewal == licenses_at_next_renewal: - raise JsonableError( - _( - "Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses." - ).format(licenses_at_next_renewal=licenses_at_next_renewal) - ) - validate_licenses( - plan.charge_automatically, - licenses_at_next_renewal, - get_latest_seat_count(user.realm), - plan.customer.exempt_from_license_number_check, - ) - update_license_ledger_for_manual_plan( - plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal - ) - return json_success(request) - - raise JsonableError(_("Nothing to change.")) + billing_session = RealmBillingSession(user=user) + billing_session.do_update_plan(update_plan_request) + return json_success(request) diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index e5c20c720a..d24470d8b6 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -39,7 +39,7 @@ from zerver.models import ( from zerver.tornado.django_api import send_event, send_event_on_commit if settings.BILLING_ENABLED: - from corporate.lib.stripe import downgrade_now_without_creating_additional_invoices + from corporate.lib.stripe import RealmBillingSession def active_humans_in_realm(realm: Realm) -> QuerySet[UserProfile]: @@ -309,7 +309,8 @@ def do_deactivate_realm(realm: Realm, *, acting_user: Optional[UserProfile]) -> realm.save(update_fields=["deactivated"]) if settings.BILLING_ENABLED: - downgrade_now_without_creating_additional_invoices(realm) + billing_session = RealmBillingSession(user=acting_user, realm=realm) + billing_session.downgrade_now_without_creating_additional_invoices() event_time = timezone_now() RealmAuditLog.objects.create( @@ -389,7 +390,8 @@ def do_delete_all_realm_attachments(realm: Realm, *, batch_size: int = 1000) -> def do_scrub_realm(realm: Realm, *, acting_user: Optional[UserProfile]) -> None: if settings.BILLING_ENABLED: - downgrade_now_without_creating_additional_invoices(realm) + billing_session = RealmBillingSession(user=acting_user, realm=realm) + billing_session.downgrade_now_without_creating_additional_invoices() users = UserProfile.objects.filter(realm=realm) for user in users: