mirror of https://github.com/zulip/zulip.git
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:
parent
4fb564026d
commit
5135acd9e3
|
@ -713,77 +713,84 @@ 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)
|
||||
|
||||
with mock.patch(
|
||||
"analytics.views.support.RealmBillingSession.downgrade_now_without_creating_additional_invoices"
|
||||
) as m:
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_now_without_additional_licenses",
|
||||
},
|
||||
)
|
||||
m.assert_called_once()
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded without creating additional invoices"], result
|
||||
)
|
||||
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/")
|
||||
|
||||
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
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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)
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
result = self.client_post(
|
||||
"/activity/support",
|
||||
{
|
||||
"realm_id": f"{iago.realm_id}",
|
||||
"modify_plan": "downgrade_now_without_additional_licenses",
|
||||
},
|
||||
)
|
||||
self.assert_in_success_response(
|
||||
["zulip downgraded without creating additional invoices"], 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")
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue