support: Use process_support_view_request for plan modifications.

Updates the support view to use process_support_view_request to
process upgrade or downgrade modifications currently implemented
for active plans.
This commit is contained in:
Lauryn Menard 2023-12-01 19:45:11 +01:00 committed by Tim Abbott
parent 4fb564026d
commit 5135acd9e3
14 changed files with 130 additions and 89 deletions

View File

@ -713,36 +713,69 @@ class TestSupportEndpoint(ZulipTestCase):
["Subdomain reserved. Please choose a different one."], result
)
def test_downgrade_realm(self) -> None:
def test_modify_plan_for_downgrade_at_end_of_billing_cycle(self) -> None:
realm = get_realm("zulip")
cordelia = self.example_user("cordelia")
self.login_user(cordelia)
result = self.client_post(
"/activity/support", {"realm_id": f"{cordelia.realm_id}", "plan_type": "2"}
"/activity/support",
{"realm_id": f"{realm.id}", "modify_plan": "downgrade_at_billing_cycle_end"},
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
CustomerPlan.objects.create(
customer=customer,
status=CustomerPlan.ACTIVE,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
tier=CustomerPlan.TIER_CLOUD_STANDARD,
)
iago = self.example_user("iago")
self.login_user(iago)
with mock.patch(
"analytics.views.support.RealmBillingSession.downgrade_at_the_end_of_billing_cycle"
) as m:
with self.assertLogs("corporate.stripe", "INFO") as m:
result = self.client_post(
"/activity/support",
{
"realm_id": f"{iago.realm_id}",
"realm_id": f"{realm.id}",
"modify_plan": "downgrade_at_billing_cycle_end",
},
)
m.assert_called_once()
self.assert_in_success_response(
["zulip marked for downgrade at the end of billing cycle"], result
)
plan = get_current_plan_by_realm(realm)
assert plan is not None
self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {customer.id}, CustomerPlan.id: {plan.id}, status: {CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}"
self.assertEqual(m.output[0], expected_log)
def test_modify_plan_for_downgrade_now_without_additional_licenses(self) -> None:
realm = get_realm("zulip")
cordelia = self.example_user("cordelia")
self.login_user(cordelia)
result = self.client_post(
"/activity/support",
{"realm_id": f"{realm.id}", "modify_plan": "downgrade_now_without_additional_licenses"},
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], "/login/")
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
plan = CustomerPlan.objects.create(
customer=customer,
status=CustomerPlan.ACTIVE,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.BILLING_SCHEDULE_ANNUAL,
tier=CustomerPlan.TIER_CLOUD_STANDARD,
)
iago = self.example_user("iago")
self.login_user(iago)
with mock.patch(
"analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices"
) as m:
result = self.client_post(
"/activity/support",
{
@ -750,40 +783,14 @@ class TestSupportEndpoint(ZulipTestCase):
"modify_plan": "downgrade_now_without_additional_licenses",
},
)
m.assert_called_once()
self.assert_in_success_response(
["zulip downgraded without creating additional invoices"], result
)
with mock.patch(
"analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices"
) as m1:
with mock.patch(
"analytics.views.support.RealmBillingSession.void_all_open_invoices", return_value=1
) as m2:
result = self.client_post(
"/activity/support",
{
"realm_id": f"{iago.realm_id}",
"modify_plan": "downgrade_now_void_open_invoices",
},
)
m1.assert_called_once()
m2.assert_called_once()
self.assert_in_success_response(
["zulip downgraded and voided 1 open invoices"], result
)
with mock.patch("analytics.views.support.switch_realm_from_standard_to_plus_plan") as m:
result = self.client_post(
"/activity/support",
{
"realm_id": f"{iago.realm_id}",
"modify_plan": "upgrade_to_plus",
},
)
m.assert_called_once_with(get_realm("zulip"))
self.assert_in_success_response(["zulip upgraded to Plus"], result)
plan.refresh_from_db()
self.assertEqual(plan.status, CustomerPlan.ENDED)
realm.refresh_from_db()
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
def test_scrub_realm(self) -> None:
cordelia = self.example_user("cordelia")

View File

@ -59,10 +59,7 @@ if settings.BILLING_ENABLED:
SupportViewRequest,
get_latest_seat_count,
)
from corporate.lib.support import (
get_discount_for_realm,
switch_realm_from_standard_to_plus_plan,
)
from corporate.lib.support import get_discount_for_realm
from corporate.models import (
Customer,
CustomerPlan,
@ -130,7 +127,7 @@ VALID_MODIFY_PLAN_METHODS = [
"downgrade_at_billing_cycle_end",
"downgrade_now_without_additional_licenses",
"downgrade_now_void_open_invoices",
"upgrade_to_plus",
"upgrade_plan_tier",
]
VALID_STATUS_VALUES = [
@ -213,6 +210,13 @@ def support(
support_type=SupportType.update_billing_modality,
billing_modality=billing_modality,
)
elif modify_plan is not None:
support_view_request = SupportViewRequest(
support_type=SupportType.modify_plan,
plan_modification=modify_plan,
)
if modify_plan == "upgrade_plan_tier":
support_view_request["new_plan_tier"] = CustomerPlan.TIER_CLOUD_PLUS
elif plan_type is not None:
current_plan_type = realm.plan_type
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
@ -246,29 +250,6 @@ def support(
elif status == "deactivated":
do_deactivate_realm(realm, acting_user=acting_user)
context["success_message"] = f"{realm.string_id} deactivated."
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":
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":
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":
billing_session.downgrade_now_without_creating_additional_invoices()
voided_invoices_count = billing_session.void_all_open_invoices()
context[
"success_message"
] = f"{realm.string_id} downgraded and voided {voided_invoices_count} open invoices"
elif modify_plan == "upgrade_to_plus":
switch_realm_from_standard_to_plus_plan(realm)
context["success_message"] = f"{realm.string_id} upgraded to Plus"
elif scrub_realm:
do_scrub_realm(realm, acting_user=acting_user)
context["success_message"] = f"{realm.string_id} scrubbed."

View File

@ -527,6 +527,7 @@ class SupportType(Enum):
update_sponsorship_status = 2
attach_discount = 3
update_billing_modality = 4
modify_plan = 5
class SupportViewRequest(TypedDict, total=False):
@ -534,6 +535,8 @@ class SupportViewRequest(TypedDict, total=False):
sponsorship_status: Optional[bool]
discount: Optional[Decimal]
billing_modality: Optional[str]
plan_modification: Optional[str]
new_plan_tier: Optional[int]
class AuditLogEventType(Enum):
@ -1859,7 +1862,7 @@ class BillingSession(ABC):
plan.next_invoice_date = next_invoice_date(plan)
plan.save(update_fields=["next_invoice_date"])
def do_change_plan_to_new_tier(self, new_plan_tier: int) -> None:
def do_change_plan_to_new_tier(self, new_plan_tier: int) -> str:
customer = self.get_customer()
assert customer is not None
current_plan = get_current_plan_by_customer(customer)
@ -1897,13 +1900,14 @@ class BillingSession(ABC):
new_plan = get_current_plan_by_customer(customer)
assert new_plan is not None # for mypy
self.invoice_plan(new_plan, plan_switch_time)
return
return f"{self.billing_entity_display_name} upgraded to {new_plan.name}"
# TODO: Implement downgrade that is a change from and to a paid plan
# tier. This should keep the same billing cycle schedule and change
# the plan when it's next invoiced vs immediately. Note this will need
# new CustomerPlan.status value, e.g. SWITCH_PLAN_TIER_AT_END_OF_CYCLE.
assert type_of_tier_change == PlanTierChangeType.DOWNGRADE # nocoverage
return "" # nocoverage
def get_event_status(self, event_status_request: EventStatusRequest) -> Dict[str, Any]:
customer = self.get_customer()
@ -2048,6 +2052,24 @@ class BillingSession(ABC):
assert support_request["billing_modality"] in VALID_BILLING_MODALITY_VALUES
charge_automatically = support_request["billing_modality"] == "charge_automatically"
success_message = self.update_billing_modality_of_current_plan(charge_automatically)
elif support_type == SupportType.modify_plan:
assert support_request["plan_modification"] is not None
plan_modification = support_request["plan_modification"]
if plan_modification == "downgrade_at_billing_cycle_end":
self.downgrade_at_the_end_of_billing_cycle()
success_message = f"{self.billing_entity_display_name} marked for downgrade at the end of billing cycle"
elif plan_modification == "downgrade_now_without_additional_licenses":
self.downgrade_now_without_creating_additional_invoices()
success_message = f"{self.billing_entity_display_name} downgraded without creating additional invoices"
elif plan_modification == "downgrade_now_void_open_invoices":
self.downgrade_now_without_creating_additional_invoices()
voided_invoices_count = self.void_all_open_invoices()
success_message = f"{self.billing_entity_display_name} downgraded and voided {voided_invoices_count} open invoices"
else:
assert plan_modification == "upgrade_plan_tier"
assert support_request["new_plan_tier"] is not None
new_plan_tier = support_request["new_plan_tier"]
success_message = self.do_change_plan_to_new_tier(new_plan_tier)
return success_message

View File

@ -5,8 +5,7 @@ from urllib.parse import urlencode, urljoin, urlunsplit
from django.conf import settings
from django.urls import reverse
from corporate.lib.stripe import RealmBillingSession
from corporate.models import CustomerPlan, get_customer_by_realm
from corporate.models import get_customer_by_realm
from zerver.models import Realm, get_realm
@ -24,8 +23,3 @@ def get_discount_for_realm(realm: Realm) -> Optional[Decimal]:
if customer is not None:
return customer.default_discount
return None
def switch_realm_from_standard_to_plus_plan(realm: Realm) -> None:
billing_session = RealmBillingSession(realm=realm)
billing_session.do_change_plan_to_new_tier(new_plan_tier=CustomerPlan.TIER_CLOUD_PLUS)

View File

@ -50,6 +50,8 @@ from corporate.lib.stripe import (
RealmBillingSession,
RemoteServerBillingSession,
StripeCardError,
SupportType,
SupportViewRequest,
add_months,
catch_stripe_errors,
compute_plan_parameters,
@ -74,7 +76,7 @@ from corporate.lib.stripe import (
update_license_ledger_for_manual_plan,
update_license_ledger_if_needed,
)
from corporate.lib.support import get_discount_for_realm, switch_realm_from_standard_to_plus_plan
from corporate.lib.support import get_discount_for_realm
from corporate.models import (
Customer,
CustomerPlan,
@ -5298,8 +5300,43 @@ class TestSupportBillingHelpers(StripeTestCase):
assert original_plan is not None
self.assertEqual(original_plan.tier, CustomerPlan.TIER_CLOUD_STANDARD)
switch_realm_from_standard_to_plus_plan(user.realm)
support_admin = self.example_user("iago")
billing_session = RealmBillingSession(
user=support_admin, realm=user.realm, support_session=True
)
support_request = SupportViewRequest(
support_type=SupportType.modify_plan,
plan_modification="upgrade_plan_tier",
new_plan_tier=CustomerPlan.TIER_CLOUD_PLUS,
)
success_message = billing_session.process_support_view_request(support_request)
self.assertEqual(success_message, "zulip upgraded to Zulip Plus")
customer.refresh_from_db()
new_plan = get_current_plan_by_customer(customer)
assert new_plan is not None
self.assertEqual(new_plan.tier, CustomerPlan.TIER_CLOUD_PLUS)
@mock_stripe()
def test_downgrade_realm_and_void_open_invoices(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
self.login_user(user)
with time_machine.travel(self.now, tick=False):
self.upgrade(invoice=True)
customer = get_customer_by_realm(user.realm)
assert customer is not None
original_plan = get_current_plan_by_customer(customer)
assert original_plan is not None
self.assertEqual(original_plan.status, CustomerPlan.ACTIVE)
support_admin = self.example_user("iago")
billing_session = RealmBillingSession(
user=support_admin, realm=user.realm, support_session=True
)
support_request = SupportViewRequest(
support_type=SupportType.modify_plan,
plan_modification="downgrade_now_void_open_invoices",
)
success_message = billing_session.process_support_view_request(support_request)
self.assertEqual(success_message, "zulip downgraded and voided 1 open invoices")
original_plan.refresh_from_db()
self.assertEqual(original_plan.status, CustomerPlan.ENDED)

View File

@ -145,7 +145,7 @@
<option value="downgrade_at_billing_cycle_end">Downgrade at the end of current billing cycle</option>
<option value="downgrade_now_without_additional_licenses">Downgrade now without creating additional invoices</option>
<option value="downgrade_now_void_open_invoices">Downgrade now and void open invoices</option>
<option value="upgrade_to_plus">Upgrade to the Plus plan</option>
<option value="upgrade_plan_tier">Upgrade to the Plus plan</option>
</select>
<button type="submit" class="btn btn-default support-submit-button">Modify</button>
</form>