support: Add admin support for updating end date of active plan.

This currently will only apply to tier.SELF_HOSTED_LEGACY plans.
This commit is contained in:
Lauryn Menard 2024-01-08 20:34:16 +01:00 committed by Tim Abbott
parent 71263ac2ab
commit 2994685399
5 changed files with 101 additions and 2 deletions

View File

@ -336,6 +336,44 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
check_no_sponsorship_request(result) check_no_sponsorship_request(result)
check_legacy_plan_without_upgrade(result) check_legacy_plan_without_upgrade(result)
def test_extend_current_plan_end_date(self) -> None:
remote_realm = RemoteRealm.objects.get(name="realm-name-5")
customer = Customer.objects.get(remote_realm=remote_realm)
plan = get_current_plan_by_customer(customer)
assert plan is not None
self.assertEqual(plan.status, CustomerPlan.ACTIVE)
self.assertEqual(plan.end_date, datetime(2050, 2, 1, tzinfo=timezone.utc))
cordelia = self.example_user("cordelia")
self.login_user(cordelia)
result = self.client_post(
"/activity/remote/support",
{"realm_id": f"{remote_realm.id}", "end_date": "2040-01-01"},
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
iago = self.example_user("iago")
self.login_user(iago)
result = self.client_post(
"/activity/remote/support",
{"remote_realm_id": f"{remote_realm.id}", "plan_end_date": "2040-01-01"},
)
self.assert_in_success_response(
["Current plan for realm-name-5 updated to end on 2040-01-01."], result
)
plan.refresh_from_db()
self.assertEqual(plan.end_date, datetime(2040, 1, 1, tzinfo=timezone.utc))
result = self.client_post(
"/activity/remote/support",
{"remote_realm_id": f"{remote_realm.id}", "plan_end_date": "2020-01-01"},
)
self.assert_in_success_response(
["Cannot update current plan for realm-name-5 to end on 2020-01-01."], result
)
class TestSupportEndpoint(ZulipTestCase): class TestSupportEndpoint(ZulipTestCase):
def create_customer_and_plan(self, realm: Realm, monthly: bool = False) -> Customer: def create_customer_and_plan(self, realm: Realm, monthly: bool = False) -> Customer:

View File

@ -33,7 +33,13 @@ from zerver.lib.exceptions import JsonableError
from zerver.lib.realm_icon import realm_icon_url from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.subdomains import get_subdomain_from_hostname from zerver.lib.subdomains import get_subdomain_from_hostname
from zerver.lib.validator import check_bool, check_string_in, to_decimal, to_non_negative_int from zerver.lib.validator import (
check_bool,
check_date,
check_string_in,
to_decimal,
to_non_negative_int,
)
from zerver.models import ( from zerver.models import (
MultiuseInvite, MultiuseInvite,
PreregistrationRealm, PreregistrationRealm,
@ -416,6 +422,7 @@ def remote_servers_support(
billing_modality: Optional[str] = REQ( billing_modality: Optional[str] = REQ(
default=None, str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES) default=None, str_validator=check_string_in(VALID_BILLING_MODALITY_VALUES)
), ),
plan_end_date: Optional[str] = REQ(default=None, str_validator=check_date),
modify_plan: Optional[str] = REQ( modify_plan: Optional[str] = REQ(
default=None, str_validator=check_string_in(VALID_MODIFY_PLAN_METHODS) default=None, str_validator=check_string_in(VALID_MODIFY_PLAN_METHODS)
), ),
@ -470,6 +477,11 @@ def remote_servers_support(
support_type=SupportType.update_billing_modality, support_type=SupportType.update_billing_modality,
billing_modality=billing_modality, billing_modality=billing_modality,
) )
elif plan_end_date is not None:
support_view_request = SupportViewRequest(
support_type=SupportType.update_plan_end_date,
plan_end_date=plan_end_date,
)
elif modify_plan is not None: elif modify_plan is not None:
support_view_request = SupportViewRequest( support_view_request = SupportViewRequest(
support_type=SupportType.modify_plan, support_type=SupportType.modify_plan,

View File

@ -4,7 +4,7 @@ import os
import secrets import secrets
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from enum import Enum from enum import Enum
from functools import wraps from functools import wraps
@ -554,6 +554,7 @@ class SupportType(Enum):
update_billing_modality = 4 update_billing_modality = 4
modify_plan = 5 modify_plan = 5
update_minimum_licenses = 6 update_minimum_licenses = 6
update_plan_end_date = 7
class SupportViewRequest(TypedDict, total=False): class SupportViewRequest(TypedDict, total=False):
@ -564,6 +565,7 @@ class SupportViewRequest(TypedDict, total=False):
plan_modification: Optional[str] plan_modification: Optional[str]
new_plan_tier: Optional[int] new_plan_tier: Optional[int]
minimum_licenses: Optional[int] minimum_licenses: Optional[int]
plan_end_date: Optional[str]
class AuditLogEventType(Enum): class AuditLogEventType(Enum):
@ -578,6 +580,7 @@ class AuditLogEventType(Enum):
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9 CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9
BILLING_ENTITY_PLAN_TYPE_CHANGED = 10 BILLING_ENTITY_PLAN_TYPE_CHANGED = 10
MINIMUM_LICENSES_CHANGED = 11 MINIMUM_LICENSES_CHANGED = 11
CUSTOMER_PLAN_END_DATE_CHANGED = 12
class PlanTierChangeType(Enum): class PlanTierChangeType(Enum):
@ -1166,6 +1169,31 @@ class BillingSession(ABC):
success_message = f"Billing collection method of {self.billing_entity_display_name} updated to send invoice." success_message = f"Billing collection method of {self.billing_entity_display_name} updated to send invoice."
return success_message return success_message
def update_end_date_of_current_plan(self, end_date_string: str) -> str:
new_end_date = datetime.strptime(end_date_string, "%Y-%m-%d").replace(tzinfo=timezone.utc)
if new_end_date.date() <= timezone_now().date():
raise SupportRequestError(
f"Cannot update current plan for {self.billing_entity_display_name} to end on {end_date_string}."
)
customer = self.get_customer()
if customer is not None:
plan = get_current_plan_by_customer(customer)
if plan is not None:
assert plan.end_date is not None
assert plan.status == CustomerPlan.ACTIVE
old_end_date = plan.end_date.strftime("%Y-%m-%d")
plan.end_date = new_end_date
plan.save(update_fields=["end_date"])
self.write_to_audit_log(
event_type=AuditLogEventType.CUSTOMER_PLAN_END_DATE_CHANGED,
event_time=timezone_now(),
extra_data={"old_end_date": old_end_date, "new_end_date": end_date_string},
)
return f"Current plan for {self.billing_entity_display_name} updated to end on {end_date_string}."
raise SupportRequestError(
f"No current plan for {self.billing_entity_display_name}."
) # nocoverage
def setup_upgrade_payment_intent_and_charge( def setup_upgrade_payment_intent_and_charge(
self, self,
plan_tier: int, plan_tier: int,
@ -2661,6 +2689,10 @@ class BillingSession(ABC):
assert support_request["billing_modality"] in VALID_BILLING_MODALITY_VALUES assert support_request["billing_modality"] in VALID_BILLING_MODALITY_VALUES
charge_automatically = support_request["billing_modality"] == "charge_automatically" charge_automatically = support_request["billing_modality"] == "charge_automatically"
success_message = self.update_billing_modality_of_current_plan(charge_automatically) success_message = self.update_billing_modality_of_current_plan(charge_automatically)
elif support_type == SupportType.update_plan_end_date:
assert support_request["plan_end_date"] is not None
new_plan_end_date = support_request["plan_end_date"]
success_message = self.update_end_date_of_current_plan(new_plan_end_date)
elif support_type == SupportType.modify_plan: elif support_type == SupportType.modify_plan:
assert support_request["plan_modification"] is not None assert support_request["plan_modification"] is not None
plan_modification = support_request["plan_modification"] plan_modification = support_request["plan_modification"]
@ -2986,6 +3018,8 @@ class RealmBillingSession(BillingSession):
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED: elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED:
return RealmAuditLog.REALM_BILLING_MODALITY_CHANGED return RealmAuditLog.REALM_BILLING_MODALITY_CHANGED
elif event_type is AuditLogEventType.CUSTOMER_PLAN_END_DATE_CHANGED:
return RealmAuditLog.CUSTOMER_PLAN_END_DATE_CHANGED # nocoverage
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN: elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN:
return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN: elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN:
@ -3344,6 +3378,8 @@ class RemoteRealmBillingSession(BillingSession):
return RemoteRealmAuditLog.REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED return RemoteRealmAuditLog.REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED
elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED: elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED:
return RemoteRealmAuditLog.REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage return RemoteRealmAuditLog.REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage
elif event_type is AuditLogEventType.CUSTOMER_PLAN_END_DATE_CHANGED:
return RemoteRealmAuditLog.CUSTOMER_PLAN_END_DATE_CHANGED
elif event_type is AuditLogEventType.BILLING_ENTITY_PLAN_TYPE_CHANGED: elif event_type is AuditLogEventType.BILLING_ENTITY_PLAN_TYPE_CHANGED:
return RemoteRealmAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED return RemoteRealmAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED
elif ( elif (
@ -3756,6 +3792,8 @@ class RemoteServerBillingSession(BillingSession):
return RemoteZulipServerAuditLog.REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED return RemoteZulipServerAuditLog.REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED
elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED: elif event_type is AuditLogEventType.BILLING_MODALITY_CHANGED:
return RemoteZulipServerAuditLog.REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage return RemoteZulipServerAuditLog.REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage
elif event_type is AuditLogEventType.CUSTOMER_PLAN_END_DATE_CHANGED:
return RemoteZulipServerAuditLog.CUSTOMER_PLAN_END_DATE_CHANGED # nocoverage
elif event_type is AuditLogEventType.BILLING_ENTITY_PLAN_TYPE_CHANGED: elif event_type is AuditLogEventType.BILLING_ENTITY_PLAN_TYPE_CHANGED:
return RemoteZulipServerAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED return RemoteZulipServerAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED
elif ( elif (

View File

@ -9,6 +9,16 @@
<button type="submit" class="btn btn-default support-submit-button">Update</button> <button type="submit" class="btn btn-default support-submit-button">Update</button>
</form> </form>
{% if current_plan.end_date and current_plan.status == current_plan.ACTIVE %}
<form method="POST" class="remote-form">
<b>Plan end date</b><br />
{{ csrf_input }}
<input type="hidden" name="{{ remote_type }}" value="{{ remote_id }}" />
<input type="date" name="plan_end_date" value="{{ current_plan.end_date.strftime('%Y-%m-%d') }}" required />
<button type="submit" class="btn btn-default support-submit-button">Update</button>
</form>
{% endif %}
<form method="POST" class="remote-form"> <form method="POST" class="remote-form">
<b>Modify current plan</b><br /> <b>Modify current plan</b><br />
{{ csrf_input }} {{ csrf_input }}

View File

@ -102,6 +102,7 @@ class AbstractRealmAuditLog(models.Model):
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503 CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504 CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504
CUSTOMER_MINIMUM_LICENSES_CHANGED = 505 CUSTOMER_MINIMUM_LICENSES_CHANGED = 505
CUSTOMER_PLAN_END_DATE_CHANGED = 506
STREAM_CREATED = 601 STREAM_CREATED = 601
STREAM_DEACTIVATED = 602 STREAM_DEACTIVATED = 602