diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index d19b209551..dad6a8890a 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -424,6 +424,14 @@ class StripeConnectionError(BillingError): pass +class ServerDeactivateWithExistingPlanError(BillingError): # nocoverage + def __init__(self) -> None: + super().__init__( + "server deactivation with existing plan", + "", + ) + + class UpgradeWithExistingPlanError(BillingError): def __init__(self) -> None: super().__init__( @@ -3853,10 +3861,9 @@ def do_change_remote_server_plan_type(remote_server: RemoteZulipServer, plan_typ @transaction.atomic -def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None: - # TODO: This should also ensure that the server doesn't have an active plan, - # and deactivate it otherwise. (Like do_deactivate_realm does.) - +def do_deactivate_remote_server( + remote_server: RemoteZulipServer, billing_session: RemoteServerBillingSession +) -> None: if remote_server.deactivated: billing_logger.warning( "Cannot deactivate remote server with ID %d, server has already been deactivated.", @@ -3864,6 +3871,36 @@ def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None: ) return + server_plans_to_consider = CustomerPlan.objects.filter( + customer__remote_server=remote_server + ).exclude(status=CustomerPlan.ENDED) + realm_plans_to_consider = CustomerPlan.objects.filter( + customer__remote_realm__server=remote_server + ).exclude(status=CustomerPlan.ENDED) + + for possible_plan in list(server_plans_to_consider) + list(realm_plans_to_consider): + if possible_plan.tier in [ + CustomerPlan.TIER_SELF_HOSTED_BASE, + CustomerPlan.TIER_SELF_HOSTED_LEGACY, + CustomerPlan.TIER_SELF_HOSTED_COMMUNITY, + ]: # nocoverage + # No action required for free plans. + continue + + if possible_plan.status in [ + CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, + CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, + ]: # nocoverage + # No action required for plans scheduled to downgrade + # automatically. + continue + + # This customer has some sort of paid plan; ask the customer + # to downgrade their paid plan so that they get the + # communication in that flow, and then they can come back and + # deactivate their server. + raise ServerDeactivateWithExistingPlanError # nocoverage + remote_server.deactivated = True remote_server.save(update_fields=["deactivated"]) RemoteZulipServerAuditLog.objects.create( diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index be73803a66..20834b99ee 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -4604,7 +4604,8 @@ class BillingHelpersTest(ZulipTestCase): ) self.assertFalse(remote_server.deactivated) - do_deactivate_remote_server(remote_server) + billing_session = RemoteServerBillingSession(remote_server) + do_deactivate_remote_server(remote_server, billing_session) remote_server = RemoteZulipServer.objects.get(uuid=server_uuid) remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter( @@ -4615,7 +4616,7 @@ class BillingHelpersTest(ZulipTestCase): # Try to deactivate a remote server that is already deactivated with self.assertLogs("corporate.stripe", "WARN") as warning_log: - do_deactivate_remote_server(remote_server) + do_deactivate_remote_server(remote_server, billing_session) self.assertEqual( warning_log.output, [ diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 29ed0f2e74..f95504c790 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -14,6 +14,7 @@ from corporate.lib.stripe import ( RealmBillingSession, RemoteRealmBillingSession, RemoteServerBillingSession, + ServerDeactivateWithExistingPlanError, UpdatePlanRequest, do_deactivate_remote_server, ) @@ -300,11 +301,11 @@ def remote_server_deactivate_page( return HttpResponseNotAllowed(["GET", "POST"]) remote_server = billing_session.remote_server + context = { + "server_hostname": remote_server.hostname, + "action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]), + } if request.method == "GET": - context = { - "server_hostname": remote_server.hostname, - "action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]), - } return render(request, "corporate/remote_billing_server_deactivate.html", context=context) assert request.method == "POST" @@ -312,7 +313,12 @@ def remote_server_deactivate_page( # Should be impossible if the user is using the UI. raise JsonableError(_("Parameter 'confirmed' is required")) - do_deactivate_remote_server(remote_server) + try: + do_deactivate_remote_server(remote_server, billing_session) + except ServerDeactivateWithExistingPlanError: # nocoverage + context["show_existing_plan_error"] = "true" + return render(request, "corporate/remote_billing_server_deactivate.html", context=context) + return render( request, "corporate/remote_billing_server_deactivated_success.html", diff --git a/templates/corporate/remote_billing_server_deactivate.html b/templates/corporate/remote_billing_server_deactivate.html index 743cabf08f..73f2166e4e 100644 --- a/templates/corporate/remote_billing_server_deactivate.html +++ b/templates/corporate/remote_billing_server_deactivate.html @@ -15,6 +15,13 @@
+ {% if show_existing_plan_error %} +
+ Could not deactivate registration. You must first + cancel + all paid plans associated with this server, including scheduled plan upgrades. +
+ {% endif %}
{{ csrf_input }}
diff --git a/web/styles/portico/billing.css b/web/styles/portico/billing.css index bafb377ca3..2c90af5e5d 100644 --- a/web/styles/portico/billing.css +++ b/web/styles/portico/billing.css @@ -669,6 +669,7 @@ input[name="licenses"] { text-align: left; } +#server-deactivate-error, #server-login-error, #autopay-error { font-weight: 400; @@ -697,6 +698,7 @@ input[name="licenses"] { top: 5px; } +#server-deactivate-error, #server-login-error { text-align: left; margin: 0 auto; diff --git a/web/styles/portico/portico_signin.css b/web/styles/portico/portico_signin.css index 7c69b278a4..8ad65ebd4b 100644 --- a/web/styles/portico/portico_signin.css +++ b/web/styles/portico/portico_signin.css @@ -479,6 +479,7 @@ html { .alert { &:not( + #server-deactivate-error, #server-login-error, .alert-info, .billing-page-success, diff --git a/zilencer/views.py b/zilencer/views.py index 1dfda4c3f8..7e504405e1 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -92,7 +92,8 @@ def deactivate_remote_server( request: HttpRequest, remote_server: RemoteZulipServer, ) -> HttpResponse: - do_deactivate_remote_server(remote_server) + billing_session = RemoteServerBillingSession(remote_server) + do_deactivate_remote_server(remote_server, billing_session) return json_success(request)