diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 46c61a99c2..2438213158 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -579,6 +579,7 @@ class SupportType(Enum): update_required_plan_tier = 8 configure_fixed_price_plan = 9 delete_fixed_price_next_plan = 10 + configure_temporary_courtesy_plan = 11 class SupportViewRequest(TypedDict, total=False): @@ -1411,6 +1412,28 @@ class BillingSession(ABC): plan_tier_name = CustomerPlan.name_from_tier(new_plan_tier) return f"Required plan tier for {self.billing_entity_display_name} set to {plan_tier_name}." + def configure_temporary_courtesy_plan(self, end_date_string: str) -> str: + plan_end_date = datetime.strptime(end_date_string, "%Y-%m-%d").replace(tzinfo=timezone.utc) + if plan_end_date.date() <= timezone_now().date(): + raise SupportRequestError( + f"Cannot configure a courtesy 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: + raise SupportRequestError( + f"Cannot configure a courtesy plan for {self.billing_entity_display_name} because of current plan." + ) + plan_anchor_date = timezone_now() + if isinstance(self, RealmBillingSession): + raise SupportRequestError( + f"Cannot currently configure a courtesy plan for {self.billing_entity_display_name}." + ) # nocoverage + + self.migrate_customer_to_legacy_plan(plan_anchor_date, plan_end_date) + return f"Temporary courtesy plan for {self.billing_entity_display_name} configured to end on {end_date_string}." + def configure_fixed_price_plan(self, fixed_price: int, sent_invoice_id: str | None) -> str: customer = self.get_customer() if customer is None: @@ -3384,6 +3407,10 @@ class BillingSession(ABC): new_fixed_price = support_request["fixed_price"] sent_invoice_id = support_request["sent_invoice_id"] success_message = self.configure_fixed_price_plan(new_fixed_price, sent_invoice_id) + elif support_type == SupportType.configure_temporary_courtesy_plan: + assert support_request["plan_end_date"] is not None + temporary_plan_end_date = support_request["plan_end_date"] + success_message = self.configure_temporary_courtesy_plan(temporary_plan_end_date) elif support_type == SupportType.update_billing_modality: assert support_request["billing_modality"] is not None assert support_request["billing_modality"] in VALID_BILLING_MODALITY_VALUES diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index a76dfd07fc..7840aec9b8 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -7352,7 +7352,9 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): self.assertEqual(success_message, "Fixed price offer deleted") result = self.client_get("/activity/remote/support", {"q": "example.com"}) self.assert_not_in_success_response(["Next plan information:"], result) - self.assert_in_success_response(["Fixed price", "Annual amount in dollars"], result) + self.assert_in_success_response( + ["Configure fixed price plan", "Annual amount in dollars"], result + ) @responses.activate @mock_stripe() diff --git a/corporate/tests/test_support_views.py b/corporate/tests/test_support_views.py index aef416e207..37752f8793 100644 --- a/corporate/tests/test_support_views.py +++ b/corporate/tests/test_support_views.py @@ -503,6 +503,50 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase): ["Cannot update current plan for realm-name-5 to end on 2020-01-01."], result ) + def test_configure_temporary_courtesy_plan(self) -> None: + iago = self.example_user("iago") + self.login_user(iago) + remote_realm = RemoteRealm.objects.get(name="realm-name-4") + # Cannot configure courtesy plan to end in the past. + result = self.client_post( + "/activity/remote/support", + { + "remote_realm_id": f"{remote_realm.id}", + "temporary_courtesy_plan": "2010-03-01", + }, + ) + self.assert_in_success_response( + ["Cannot configure a courtesy plan for realm-name-4 to end on 2010-03-01."], + result, + ) + # Cannot configure courtesy plan if there is a current plan for billing entity. + result = self.client_post( + "/activity/remote/support", + { + "remote_realm_id": f"{remote_realm.id}", + "temporary_courtesy_plan": "2050-03-01", + }, + ) + self.assert_in_success_response( + ["Cannot configure a courtesy plan for realm-name-4 because of current plan."], + result, + ) + remote_realm = RemoteRealm.objects.get(name="realm-name-2") + assert remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_MANAGED + result = self.client_post( + "/activity/remote/support", + { + "remote_realm_id": f"{remote_realm.id}", + "temporary_courtesy_plan": "2050-03-01", + }, + ) + self.assert_in_success_response( + ["Temporary courtesy plan for realm-name-2 configured to end on 2050-03-01."], + result, + ) + remote_realm.refresh_from_db() + assert remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY + def test_discount_support_actions_when_upgrade_scheduled(self) -> None: remote_realm = RemoteRealm.objects.get(name="realm-name-4") billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) diff --git a/corporate/views/support.py b/corporate/views/support.py index 15745e68eb..e8af7dea7d 100644 --- a/corporate/views/support.py +++ b/corporate/views/support.py @@ -651,6 +651,10 @@ def remote_servers_support( modify_plan: VALID_MODIFY_PLAN_METHODS | None = None, delete_fixed_price_next_plan: Json[bool] = False, remote_server_status: VALID_STATUS_VALUES | None = None, + temporary_courtesy_plan: Annotated[ + str, AfterValidator(lambda x: check_date("temporary_courtesy_plan", x)) + ] + | None = None, ) -> HttpResponse: context: dict[str, Any] = {} @@ -706,6 +710,11 @@ def remote_servers_support( fixed_price=fixed_price, sent_invoice_id=sent_invoice_id, ) + elif temporary_courtesy_plan is not None: + support_view_request = SupportViewRequest( + support_type=SupportType.configure_temporary_courtesy_plan, + plan_end_date=temporary_courtesy_plan, + ) elif billing_modality is not None: support_view_request = SupportViewRequest( support_type=SupportType.update_billing_modality, diff --git a/templates/corporate/support/next_plan_forms_support.html b/templates/corporate/support/next_plan_forms_support.html index 8c28b48cfd..4efbaef627 100644 --- a/templates/corporate/support/next_plan_forms_support.html +++ b/templates/corporate/support/next_plan_forms_support.html @@ -1,14 +1,24 @@ -

⏱️ Schedule fixed price plan:

+

⏱️ Schedule plan:

- Fixed price
- {% if not is_current_plan_billable %} + Configure fixed price plan:
+ {% if not plan_data.is_current_plan_billable %} Enter Invoice ID only if the customer chose to pay by invoice.
{% endif %} {{ csrf_input }} - {% if not is_current_plan_billable %} + {% if not plan_data.is_current_plan_billable %} {% endif %}
+{% if not plan_data.current_plan %} +
+ Configure temporary courtesy plan:
+ Once created, the end date for the temporary courtesy plan can be extended.
+ {{ csrf_input }} + + + +
+{% endif %} diff --git a/templates/corporate/support/remote_realm_details.html b/templates/corporate/support/remote_realm_details.html index 96219c83bf..ca1b5b7503 100644 --- a/templates/corporate/support/remote_realm_details.html +++ b/templates/corporate/support/remote_realm_details.html @@ -94,7 +94,7 @@ {% elif not remote_server_deactivated %}
{% with %} - {% set is_current_plan_billable = support_data[remote_realm.id].plan_data.is_current_plan_billable %} + {% set plan_data = support_data[remote_realm.id].plan_data %} {% set remote_id = remote_realm.id %} {% set remote_type = "remote_realm_id" %} {% include 'corporate/support/next_plan_forms_support.html' %} diff --git a/templates/corporate/support/remote_server_support.html b/templates/corporate/support/remote_server_support.html index 3bcf39603f..af817af4b4 100644 --- a/templates/corporate/support/remote_server_support.html +++ b/templates/corporate/support/remote_server_support.html @@ -139,7 +139,7 @@ {% elif not remote_server.deactivated %}
{% with %} - {% set is_current_plan_billable = remote_servers_support_data[remote_server.id].plan_data.is_current_plan_billable %} + {% set plan_data = remote_servers_support_data[remote_server.id].plan_data %} {% set remote_id = remote_server.id %} {% set remote_type = "remote_server_id" %} {% include 'corporate/support/next_plan_forms_support.html' %}