From a3093fad9713af75e4077e9bac930898046a1ca1 Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Thu, 2 Nov 2023 15:34:37 +0000 Subject: [PATCH] billing: Show sponsorship status on /sponsorship page. We redirect users from billing and upgrade page to sponsorship page if the org has requested for sponsorship or is already sponsored. --- corporate/tests/test_stripe.py | 97 +++++++++------------------- corporate/views/billing_page.py | 28 +++++--- corporate/views/upgrade.py | 19 +++--- templates/corporate/billing.html | 16 +---- templates/corporate/sponsorship.html | 25 +++++++ 5 files changed, 83 insertions(+), 102 deletions(-) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index b2db3224e6..2d5b6c6ff7 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -22,7 +22,6 @@ from typing import ( Tuple, TypeVar, ) -from unittest import mock from unittest.mock import Mock, patch import orjson @@ -438,6 +437,11 @@ class StripeTestCase(ZulipTestCase): self.next_month = datetime(2012, 2, 2, 3, 4, 5, tzinfo=timezone.utc) self.next_year = datetime(2013, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + # Make hamlet billing admin for testing. + hamlet = self.example_user("hamlet") + hamlet.is_billing_admin = True + hamlet.save(update_fields=["is_billing_admin"]) + def get_signed_seat_count_from_response(self, response: "TestHttpResponse") -> Optional[str]: match = re.search(r"name=\"signed_seat_count\" value=\"(.+)\"", response.content.decode()) return match.group(1) if match else None @@ -2288,22 +2292,23 @@ class StripeTest(StripeTestCase): response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) - self.assertEqual(response["Location"], "/billing/") + self.assertEqual(response["Location"], "/sponsorship/") response = self.client_get("/billing/") - self.assert_in_success_response( - ["Your organization has requested sponsored or discounted hosting."], response - ) + self.assertEqual(response.status_code, 302) + self.assertEqual(response["Location"], "/sponsorship/") + + response = self.client_get("/sponsorship/") self.assert_in_success_response( [ - 'Please contact Zulip support if you have any questions or concerns.' + "Your organization has requested sponsorship for a free or discounted Zulip Cloud Standard plan." ], response, ) - # Ensure that the other "contact support" footer is not displayed, since that would be - # duplicate. - self.assert_not_in_success_response( - ['Contact Zulip support for billing history.'], + self.assert_in_success_response( + [ + 'Please feel free to contact Zulip support with any questions or updates to your request.' + ], response, ) @@ -2317,13 +2322,9 @@ class StripeTest(StripeTestCase): user.realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE user.realm.save() self.login_user(self.example_user("hamlet")) - response = self.client_get("/billing/") + response = self.client_get("/sponsorship/") self.assert_in_success_response( - ["Your organization is fully sponsored and is on the Zulip Cloud Standard"], - response, - ) - self.assert_in_success_response( - ['Contact Zulip support for billing history.'], + ["Zulip is sponsoring free Zulip Cloud Standard hosting for your organization."], response, ) @@ -2331,13 +2332,22 @@ class StripeTest(StripeTestCase): user = self.example_user("iago") self.login_user(user) response = self.client_get("/billing/") - self.assertEqual(response.status_code, 302) - self.assertEqual("/upgrade/", response["Location"]) + not_admin_message = ( + "You must be an organization owner or a billing administrator to view this page." + ) + self.assert_in_success_response([not_admin_message], response) user.realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE user.realm.save() response = self.client_get("/billing/") - self.assertEqual(response.status_code, 200) + self.assert_in_success_response([not_admin_message], response) + + # Billing page redirects to sponsorship page for standard free admins. + user = self.example_user("hamlet") + self.login_user(user) + response = self.client_get("/billing/") + self.assertEqual(response.status_code, 302) + self.assertEqual("/sponsorship/", response["Location"]) user.realm.plan_type = Realm.PLAN_TYPE_LIMITED user.realm.save() @@ -2347,7 +2357,7 @@ class StripeTest(StripeTestCase): self.assertEqual("/upgrade/", response["Location"]) def test_upgrade_page_for_demo_organizations(self) -> None: - user = self.example_user("iago") + user = self.example_user("hamlet") user.realm.demo_organization_scheduled_deletion_date = timezone_now() + timedelta(days=30) user.realm.save() self.login_user(user) @@ -2366,7 +2376,7 @@ class StripeTest(StripeTestCase): user.realm.save() response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) - self.assertEqual(response["Location"], "/billing/") + self.assertEqual(response["Location"], "/sponsorship/") user.realm.plan_type = Realm.PLAN_TYPE_LIMITED user.realm.save() @@ -3808,47 +3818,6 @@ class StripeTest(StripeTestCase): (invoice,) = stripe.Invoice.list(customer=stripe_customer_id) self.assertEqual(invoice.amount_due, 7200) - def test_request_sponsorship_available_on_upgrade_and_billing_pages(self) -> None: - """ - Verifies that the Request sponsorship form is available on both the upgrade - page and the billing page for already subscribed customers. - """ - realm = get_realm("zulip") - self.login("desdemona") - result = self.client_get("/upgrade/") - self.assert_in_success_response(["Request sponsorship"], 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.ANNUAL, - tier=CustomerPlan.STANDARD, - price_per_license=1000, - ) - LicenseLedger.objects.create( - plan=plan, - is_renewal=True, - event_time=timezone_now(), - licenses=9, - licenses_at_next_renewal=9, - ) - - mock_stripe_customer = mock.MagicMock() - mock_stripe_customer.email = "desdemona@zulip.com" - - with mock.patch( - "corporate.views.billing_page.stripe_get_customer", return_value=mock_stripe_customer - ): - result = self.client_get("/billing/") - # Sanity assert to make sure we're testing the subscribed billing page. - self.assert_in_success_response( - ["Your current plan is Zulip Cloud Standard."], result - ) - - self.assert_in_success_response(["Request sponsorship"], result) - @mock_stripe() def test_customer_has_credit_card_as_default_payment_method(self, *mocks: Mock) -> None: iago = self.example_user("iago") @@ -4046,10 +4015,6 @@ class RequiresBillingAccessTest(StripeTestCase): @override def setUp(self, *mocks: Mock) -> None: super().setUp() - hamlet = self.example_user("hamlet") - hamlet.is_billing_admin = True - hamlet.save(update_fields=["is_billing_admin"]) - desdemona = self.example_user("desdemona") desdemona.role = UserProfile.ROLE_REALM_OWNER desdemona.save(update_fields=["role"]) diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 1c1c71f165..2c3b604d97 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -81,6 +81,14 @@ def sponsorship_request(request: HttpRequest) -> HttpResponse: user = request.user assert user.is_authenticated context: Dict[str, Any] = {} + + customer = get_customer_by_realm(user.realm) + if customer is not None and customer.sponsorship_pending: + context["is_sponsorship_pending"] = True + + if user.realm.plan_type == user.realm.PLAN_TYPE_STANDARD_FREE: + context["is_sponsored"] = True + add_sponsorship_info_to_context(context, user) return render(request, "corporate/sponsorship.html", context=context) @@ -93,33 +101,34 @@ def billing_home( user = request.user assert user.is_authenticated - customer = get_customer_by_realm(user.realm) context: Dict[str, Any] = { "admin_access": user.has_billing_access, "has_active_plan": False, } + if not user.has_billing_access: + return render(request, "corporate/billing.html", context=context) + if user.realm.plan_type == user.realm.PLAN_TYPE_STANDARD_FREE: context["is_sponsored"] = True - return render(request, "corporate/billing.html", context=context) + return HttpResponseRedirect(reverse("sponsorship_request")) + + customer = get_customer_by_realm(user.realm) + if ( + customer is not None and customer.sponsorship_pending + ) or user.realm.plan_type == user.realm.PLAN_TYPE_STANDARD_FREE: + return HttpResponseRedirect(reverse("sponsorship_request")) if customer is None: from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) - if customer.sponsorship_pending: - context["sponsorship_pending"] = True - return render(request, "corporate/billing.html", context=context) - if not CustomerPlan.objects.filter(customer=customer).exists(): from corporate.views.upgrade import initial_upgrade return HttpResponseRedirect(reverse(initial_upgrade)) - if not user.has_billing_access: - return render(request, "corporate/billing.html", context=context) - plan = get_current_plan_by_customer(customer) if plan is not None: now = timezone_now() @@ -172,7 +181,6 @@ def billing_home( CustomerPlan=CustomerPlan, onboarding=onboarding, ) - add_sponsorship_info_to_context(context, user) return render(request, "corporate/billing.html", context=context) diff --git a/corporate/views/upgrade.py b/corporate/views/upgrade.py index 2315773672..bce4185146 100644 --- a/corporate/views/upgrade.py +++ b/corporate/views/upgrade.py @@ -20,7 +20,6 @@ from corporate.lib.stripe import ( get_latest_seat_count, get_price_per_license, is_free_trial_offer_enabled, - is_sponsored_realm, process_initial_upgrade, sign_string, unsign_string, @@ -34,7 +33,7 @@ from corporate.models import ( get_current_plan_by_customer, get_customer_by_realm, ) -from corporate.views.billing_page import add_sponsorship_info_to_context, billing_home +from corporate.views.billing_page import billing_home from zerver.actions.users import do_make_user_billing_admin from zerver.decorator import require_organization_member, zulip_login_required from zerver.lib.request import REQ, has_request_variables @@ -231,19 +230,18 @@ def initial_upgrade( if not settings.BILLING_ENABLED or user.is_guest: return render(request, "404.html", status=404) - billing_page_url = reverse(billing_home) - customer = get_customer_by_realm(user.realm) - if customer is not None and ( - get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending - ): + if ( + customer is not None and customer.sponsorship_pending + ) or user.realm.plan_type == user.realm.PLAN_TYPE_STANDARD_FREE: + return HttpResponseRedirect(reverse("sponsorship_request")) + + billing_page_url = reverse(billing_home) + if customer is not None and (get_current_plan_by_customer(customer) is not None or onboarding): if onboarding: billing_page_url = f"{billing_page_url}?onboarding=true" return HttpResponseRedirect(billing_page_url) - if is_sponsored_realm(user.realm): - return HttpResponseRedirect(billing_page_url) - percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: percent_off = customer.default_discount @@ -275,7 +273,6 @@ def initial_upgrade( }, "is_demo_organization": user.realm.demo_organization_scheduled_deletion_date is not None, } - add_sponsorship_info_to_context(context, user) response = render(request, "corporate/upgrade.html", context=context) return response diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index 7fbd93266b..ac29e09db5 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -188,18 +188,8 @@

{% elif admin_access and not has_active_plan %} +
- {% if sponsorship_pending %} -

Your organization has requested sponsored or discounted hosting.

-
- Please contact Zulip support if you have any questions or concerns. - {% elif is_sponsored %} -
-

-

💚 Your organization is fully sponsored and is on the Zulip Cloud Standard plan.

-

-
- {% else %}

Your organization is on the Zulip Free plan.

@@ -211,16 +201,12 @@

- {% endif %}
- {% if not sponsorship_pending %} - {# In the other case, we're displaying a different message about contacting support. #} - {% endif %} {% else %}

You must be an organization owner or a billing administrator to view this page. diff --git a/templates/corporate/sponsorship.html b/templates/corporate/sponsorship.html index f931668aa1..8829bc9f82 100644 --- a/templates/corporate/sponsorship.html +++ b/templates/corporate/sponsorship.html @@ -4,6 +4,30 @@ {% set PAGE_TITLE = "💚 Request sponsorship" %} {% block portico_content %} + +{% if is_sponsored %} + +

+
+

Zulip Cloud billing for {{realm_name}}

+

Zulip is sponsoring free Zulip Cloud Standard hosting for your organization. 🎉

+
+
+ +{% elif is_sponsorship_pending %} + +
+
+

Sponsorship request pending for {{realm_name}}

+

Your organization has requested sponsorship for a free or discounted Zulip Cloud Standard plan.

+

+ Please feel free to contact Zulip support with any questions or updates to your request. +

+
+
+ +{% else %} +
@@ -55,5 +79,6 @@
+{% endif %} {% endblock %}