server_deactivate: Show error message for server on active plan.

This commit is contained in:
Aman Agrawal 2023-12-13 01:44:55 +00:00 committed by Tim Abbott
parent c2636354a5
commit 63f4fc51de
7 changed files with 67 additions and 12 deletions

View File

@ -424,6 +424,14 @@ class StripeConnectionError(BillingError):
pass pass
class ServerDeactivateWithExistingPlanError(BillingError): # nocoverage
def __init__(self) -> None:
super().__init__(
"server deactivation with existing plan",
"",
)
class UpgradeWithExistingPlanError(BillingError): class UpgradeWithExistingPlanError(BillingError):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__( super().__init__(
@ -3853,10 +3861,9 @@ def do_change_remote_server_plan_type(remote_server: RemoteZulipServer, plan_typ
@transaction.atomic @transaction.atomic
def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None: def do_deactivate_remote_server(
# TODO: This should also ensure that the server doesn't have an active plan, remote_server: RemoteZulipServer, billing_session: RemoteServerBillingSession
# and deactivate it otherwise. (Like do_deactivate_realm does.) ) -> None:
if remote_server.deactivated: if remote_server.deactivated:
billing_logger.warning( billing_logger.warning(
"Cannot deactivate remote server with ID %d, server has already been deactivated.", "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 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.deactivated = True
remote_server.save(update_fields=["deactivated"]) remote_server.save(update_fields=["deactivated"])
RemoteZulipServerAuditLog.objects.create( RemoteZulipServerAuditLog.objects.create(

View File

@ -4604,7 +4604,8 @@ class BillingHelpersTest(ZulipTestCase):
) )
self.assertFalse(remote_server.deactivated) 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_server = RemoteZulipServer.objects.get(uuid=server_uuid)
remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter( remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter(
@ -4615,7 +4616,7 @@ class BillingHelpersTest(ZulipTestCase):
# Try to deactivate a remote server that is already deactivated # Try to deactivate a remote server that is already deactivated
with self.assertLogs("corporate.stripe", "WARN") as warning_log: 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( self.assertEqual(
warning_log.output, warning_log.output,
[ [

View File

@ -14,6 +14,7 @@ from corporate.lib.stripe import (
RealmBillingSession, RealmBillingSession,
RemoteRealmBillingSession, RemoteRealmBillingSession,
RemoteServerBillingSession, RemoteServerBillingSession,
ServerDeactivateWithExistingPlanError,
UpdatePlanRequest, UpdatePlanRequest,
do_deactivate_remote_server, do_deactivate_remote_server,
) )
@ -300,11 +301,11 @@ def remote_server_deactivate_page(
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])
remote_server = billing_session.remote_server remote_server = billing_session.remote_server
if request.method == "GET":
context = { context = {
"server_hostname": remote_server.hostname, "server_hostname": remote_server.hostname,
"action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]), "action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]),
} }
if request.method == "GET":
return render(request, "corporate/remote_billing_server_deactivate.html", context=context) return render(request, "corporate/remote_billing_server_deactivate.html", context=context)
assert request.method == "POST" assert request.method == "POST"
@ -312,7 +313,12 @@ def remote_server_deactivate_page(
# Should be impossible if the user is using the UI. # Should be impossible if the user is using the UI.
raise JsonableError(_("Parameter 'confirmed' is required")) 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( return render(
request, request,
"corporate/remote_billing_server_deactivated_success.html", "corporate/remote_billing_server_deactivated_success.html",

View File

@ -15,6 +15,13 @@
</div> </div>
<div class="white-box"> <div class="white-box">
<div id="server-deactivate-details"> <div id="server-deactivate-details">
{% if show_existing_plan_error %}
<div id="server-deactivate-error" class="alert alert-danger">
Could not deactivate registration. You must first
<a href="https://zulip.com/help/self-hosted-billing#cancel-paid-plan">cancel</a>
all paid plans associated with this server, including scheduled plan upgrades.
</div>
{% endif %}
<form id="server-deactivate-form" method="post" action="{{ action_url }}"> <form id="server-deactivate-form" method="post" action="{{ action_url }}">
{{ csrf_input }} {{ csrf_input }}
<div id="server-deactivate-form-top-description" class="input-box server-deactivate-form-field no-validation"> <div id="server-deactivate-form-top-description" class="input-box server-deactivate-form-field no-validation">

View File

@ -669,6 +669,7 @@ input[name="licenses"] {
text-align: left; text-align: left;
} }
#server-deactivate-error,
#server-login-error, #server-login-error,
#autopay-error { #autopay-error {
font-weight: 400; font-weight: 400;
@ -697,6 +698,7 @@ input[name="licenses"] {
top: 5px; top: 5px;
} }
#server-deactivate-error,
#server-login-error { #server-login-error {
text-align: left; text-align: left;
margin: 0 auto; margin: 0 auto;

View File

@ -479,6 +479,7 @@ html {
.alert { .alert {
&:not( &:not(
#server-deactivate-error,
#server-login-error, #server-login-error,
.alert-info, .alert-info,
.billing-page-success, .billing-page-success,

View File

@ -92,7 +92,8 @@ def deactivate_remote_server(
request: HttpRequest, request: HttpRequest,
remote_server: RemoteZulipServer, remote_server: RemoteZulipServer,
) -> HttpResponse: ) -> HttpResponse:
do_deactivate_remote_server(remote_server) billing_session = RemoteServerBillingSession(remote_server)
do_deactivate_remote_server(remote_server, billing_session)
return json_success(request) return json_success(request)