From 4c6350fa4b87efc7a7983f48b2f1f80c926c8ce2 Mon Sep 17 00:00:00 2001 From: Vishnu KS Date: Tue, 9 Jun 2020 15:54:32 +0530 Subject: [PATCH] billing: Add option to request a sponsorship in /upgrade. --- analytics/tests/test_views.py | 30 +++++++++ analytics/views.py | 19 +++++- corporate/lib/stripe.py | 5 ++ .../0009_customer_sponsorship_pending.py | 18 +++++ corporate/models.py | 1 + corporate/tests/test_stripe.py | 45 ++++++++++++- corporate/urls.py | 2 + corporate/views.py | 67 +++++++++++++++++-- frontend_tests/node_tests/billing_helpers.js | 5 ++ frontend_tests/node_tests/upgrade.js | 37 +++++++++- static/js/billing/helpers.js | 16 ++++- static/js/billing/upgrade.js | 9 +++ static/styles/portico/activity.scss | 5 ++ static/styles/portico/billing.scss | 8 ++- templates/analytics/realm_details.html | 10 +++ templates/corporate/billing.html | 4 ++ templates/corporate/upgrade.html | 48 +++++++++++++ .../zerver/emails/sponsorship_request.html | 24 +++++++ .../emails/sponsorship_request.subject.txt | 1 + .../zerver/emails/sponsorship_request.txt | 10 +++ templates/zerver/plans.html | 6 +- zerver/models.py | 4 ++ zerver/tests/test_docs.py | 18 +++-- zerver/tests/test_home.py | 23 ++++++- zerver/views/home.py | 10 ++- zerver/views/portico.py | 11 ++- zproject/dev_settings.py | 1 + 27 files changed, 416 insertions(+), 21 deletions(-) create mode 100644 corporate/migrations/0009_customer_sponsorship_pending.py create mode 100644 templates/zerver/emails/sponsorship_request.html create mode 100644 templates/zerver/emails/sponsorship_request.subject.txt create mode 100644 templates/zerver/emails/sponsorship_request.txt diff --git a/analytics/tests/test_views.py b/analytics/tests/test_views.py index f64ed52ba1..6e9eae0a3e 100644 --- a/analytics/tests/test_views.py +++ b/analytics/tests/test_views.py @@ -10,6 +10,7 @@ from analytics.lib.counts import COUNT_STATS, CountStat from analytics.lib.time_utils import time_range from analytics.models import FillState, RealmCount, UserCount, last_successful_fill from analytics.views import rewrite_client_arrays, sort_by_totals, sort_client_labels +from corporate.models import get_customer_by_realm from zerver.lib.actions import do_create_multiuse_invite_link, do_send_realm_reactivation_email from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import reset_emails_in_zulip_realm @@ -575,6 +576,35 @@ class TestSupportEndpoint(ZulipTestCase): m.assert_called_once_with(get_realm("lear"), 25) self.assert_in_success_response(["Discount of Lear & Co. changed to 25 from None"], result) + def test_change_sponsorship_status(self) -> None: + lear_realm = get_realm("lear") + self.assertIsNone(get_customer_by_realm(lear_realm)) + + cordelia = self.example_user('cordelia') + self.login_user(cordelia) + + result = self.client_post("/activity/support", {"realm_id": f"{lear_realm.id}", + "sponsorship_pending": "true"}) + 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/support", {"realm_id": f"{lear_realm.id}", + "sponsorship_pending": "true"}) + self.assert_in_success_response(["Lear & Co. marked as pending sponsorship."], result) + customer = get_customer_by_realm(lear_realm) + assert(customer is not None) + self.assertTrue(customer.sponsorship_pending) + + result = self.client_post("/activity/support", {"realm_id": f"{lear_realm.id}", + "sponsorship_pending": "false"}) + self.assert_in_success_response(["Lear & Co. is no longer pending sponsorship."], result) + customer = get_customer_by_realm(lear_realm) + assert(customer is not None) + self.assertFalse(customer.sponsorship_pending) + def test_activate_or_deactivate_realm(self) -> None: cordelia = self.example_user('cordelia') lear_realm = get_realm('lear') diff --git a/analytics/views.py b/analytics/views.py index 1752589839..991f218e08 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -70,7 +70,12 @@ from zerver.models import ( from zerver.views.invite import get_invitee_emails_set if settings.BILLING_ENABLED: - from corporate.lib.stripe import attach_discount_to_realm, get_discount_for_realm + from corporate.lib.stripe import ( + attach_discount_to_realm, + get_customer_by_realm, + get_discount_for_realm, + update_sponsorship_status, + ) if settings.ZILENCER_ENABLED: from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer @@ -1136,6 +1141,15 @@ def support(request: HttpRequest) -> HttpResponse: do_deactivate_realm(realm, request.user) context["message"] = f"{realm.name} deactivated." + sponsorship_pending = request.POST.get("sponsorship_pending", None) + if sponsorship_pending is not None: + if sponsorship_pending == "true": + update_sponsorship_status(realm, True) + context["message"] = f"{realm.name} marked as pending sponsorship." + elif sponsorship_pending == "false": + update_sponsorship_status(realm, False) + context["message"] = f"{realm.name} is no longer pending sponsorship." + scrub_realm = request.POST.get("scrub_realm", None) if scrub_realm is not None: if scrub_realm == "scrub_realm": @@ -1165,6 +1179,9 @@ def support(request: HttpRequest) -> HttpResponse: except ValidationError: pass + for realm in realms: + realm.customer = get_customer_by_realm(realm) + context["realms"] = realms confirmations: List[Dict[str, Any]] = [] diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 4115311b45..21d7dca314 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -558,6 +558,11 @@ def invoice_plans_as_needed(event_time: datetime=timezone_now()) -> None: def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None: Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount}) +def update_sponsorship_status(realm: Realm, sponsorship_pending: bool) -> None: + customer, _ = Customer.objects.get_or_create(realm=realm) + customer.sponsorship_pending = sponsorship_pending + customer.save(update_fields=["sponsorship_pending"]) + def get_discount_for_realm(realm: Realm) -> Optional[Decimal]: customer = get_customer_by_realm(realm) if customer is not None: diff --git a/corporate/migrations/0009_customer_sponsorship_pending.py b/corporate/migrations/0009_customer_sponsorship_pending.py new file mode 100644 index 0000000000..97cd760cec --- /dev/null +++ b/corporate/migrations/0009_customer_sponsorship_pending.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2020-06-09 12:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('corporate', '0008_nullable_next_invoice_date'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='sponsorship_pending', + field=models.BooleanField(default=False), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index f2b06ebfb2..4d065ab0bc 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -11,6 +11,7 @@ from zerver.models import Realm class Customer(models.Model): realm: Realm = models.OneToOneField(Realm, on_delete=CASCADE) stripe_customer_id: str = models.CharField(max_length=255, null=True, unique=True) + sponsorship_pending: bool = models.BooleanField(default=False) # A percentage, like 85. default_discount: Optional[Decimal] = models.DecimalField(decimal_places=4, max_digits=7, null=True) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index bad0317364..9aec5de310 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1068,6 +1068,48 @@ class StripeTest(StripeTestCase): self.assert_json_error_contains(response, "Something went wrong. Please contact desdemona+admin@zulip.com.") self.assertEqual(ujson.loads(response.content)['error_description'], 'uncaught exception during upgrade') + def test_request_sponsorship(self) -> None: + user = self.example_user("hamlet") + self.assertIsNone(get_customer_by_realm(user.realm)) + + self.login_user(user) + + data = { + "organization-type": ujson.dumps("Open-source"), + "website": ujson.dumps("https://infinispan.org/"), + "description": ujson.dumps("Infinispan is a distributed in-memory key/value data store with optional schema."), + } + response = self.client_post("/json/billing/sponsorship", data) + self.assert_json_success(response) + + customer = get_customer_by_realm(user.realm) + assert(customer is not None) + self.assertEqual(customer.sponsorship_pending, True) + from django.core.mail import outbox + self.assertEqual(len(outbox), 1) + + for message in outbox: + self.assertEqual(len(message.to), 1) + self.assertEqual(message.to[0], "desdemona+admin@zulip.com") + self.assertEqual(message.subject, "Sponsorship request (Open-source) for zulip") + self.assertEqual(message.from_email, f'{user.full_name} <{user.delivery_email}>') + self.assertIn("User role: Member", message.body) + self.assertIn("Support URL: http://zulip.testserver/activity/support?q=zulip", message.body) + self.assertIn("Website: https://infinispan.org", message.body) + self.assertIn("Organization type: Open-source", message.body) + self.assertIn("Description:\nInfinispan is a distributed in-memory", message.body) + + response = self.client_get("/upgrade/") + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/billing/") + + response = self.client_get("/billing/") + self.assert_in_success_response(["Your organization has requested sponsored or discounted hosting."], response) + + self.login_user(self.example_user("othello")) + response = self.client_get("/billing/") + self.assert_in_success_response(["You must be an organization administrator or a billing administrator to view this page."], response) + def test_redirect_for_billing_home(self) -> None: user = self.example_user("iago") self.login_user(user) @@ -1796,8 +1838,9 @@ class RequiresBillingAccessTest(ZulipTestCase): string_with_all_endpoints = str(get_resolver('corporate.urls').reverse_dict) json_endpoints = {word.strip("\"'()[],$") for word in string_with_all_endpoints.split() if 'json/' in word} - # No need to test upgrade endpoint as it only requires user to be logged in. + # No need to test upgrade and sponsorship endpoints as they only require user to be logged in. json_endpoints.remove("json/billing/upgrade") + json_endpoints.remove("json/billing/sponsorship") self.assertEqual(len(json_endpoints), len(params)) diff --git a/corporate/urls.py b/corporate/urls.py index 4ebb7c2e6b..d2c81b0eee 100644 --- a/corporate/urls.py +++ b/corporate/urls.py @@ -22,6 +22,8 @@ i18n_urlpatterns: Any = [ v1_api_and_json_patterns = [ path('billing/upgrade', rest_dispatch, {'POST': 'corporate.views.upgrade'}), + path('billing/sponsorship', rest_dispatch, + {'POST': 'corporate.views.sponsorship'}), path('billing/plan/change', rest_dispatch, {'POST': 'corporate.views.change_plan_status'}), path('billing/sources/change', rest_dispatch, diff --git a/corporate/views.py b/corporate/views.py index 0e0b5b7b6d..4ee6130f33 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -1,6 +1,7 @@ import logging from decimal import Decimal from typing import Any, Dict, Optional, Union +from urllib.parse import urlencode, urljoin, urlunsplit import stripe from django.conf import settings @@ -28,6 +29,7 @@ from corporate.lib.stripe import ( start_of_next_billing_cycle, stripe_get_customer, unsign_string, + update_sponsorship_status, ) from corporate.models import ( CustomerPlan, @@ -38,8 +40,9 @@ from corporate.models import ( from zerver.decorator import require_billing_access, zulip_login_required from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_error, json_success +from zerver.lib.send_email import FromAddress, send_email from zerver.lib.validator import check_int, check_string -from zerver.models import UserProfile +from zerver.models import UserProfile, get_realm billing_logger = logging.getLogger('corporate.stripe') @@ -145,8 +148,9 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: return render(request, "404.html") user = request.user + customer = get_customer_by_realm(user.realm) - if customer is not None and get_current_plan_by_customer(customer) is not None: + if customer is not None and (get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending): billing_page_url = reverse('corporate.views.billing_home') if request.GET.get("onboarding") is not None: billing_page_url = f"{billing_page_url}?onboarding=true" @@ -159,6 +163,7 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: seat_count = get_latest_seat_count(user.realm) signed_seat_count, salt = sign_string(str(seat_count)) context: Dict[str, Any] = { + 'realm': user.realm, 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'email': user.delivery_email, 'seat_count': seat_count, @@ -179,17 +184,71 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: response = render(request, 'corporate/upgrade.html', context=context) return response +@has_request_variables +def sponsorship(request: HttpRequest, user: UserProfile, + organization_type: str=REQ("organization-type", validator=check_string), + website: str=REQ("website", validator=check_string), + description: str=REQ("description", validator=check_string)) -> HttpResponse: + realm = user.realm + + requested_by = user.full_name + + role_id_to_name_map = { + UserProfile.ROLE_REALM_OWNER: "Realm owner", + UserProfile.ROLE_REALM_ADMINISTRATOR: "Realm adminstrator", + UserProfile.ROLE_MEMBER: "Member", + UserProfile.ROLE_GUEST: "Guest" + } + user_role = role_id_to_name_map[user.role] + + support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri + support_url = urljoin(support_realm_uri, urlunsplit(("", "", reverse('analytics.views.support'), + urlencode({"q": realm.string_id}), ""))) + + context = { + "requested_by": requested_by, + "user_role": user_role, + "string_id": realm.string_id, + "support_url": support_url, + "organization_type": organization_type, + "website": website, + "description": description, + } + send_email( + "zerver/emails/sponsorship_request", + to_emails=[FromAddress.SUPPORT], + from_name=user.full_name, + from_address=user.delivery_email, + context=context, + ) + + update_sponsorship_status(realm, True) + user.is_billing_admin = True + user.save(update_fields=["is_billing_admin"]) + + return json_success() + @zulip_login_required def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) + context: Dict[str, Any] = {} + if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) + + if customer.sponsorship_pending: + if user.has_billing_access: + context = {"admin_access": True, "sponsorship_pending": True} + else: + context = {"admin_access": False} + return render(request, 'corporate/billing.html', context=context) + if not CustomerPlan.objects.filter(customer=customer).exists(): return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) - if not user.is_realm_admin and not user.is_billing_admin: - context: Dict[str, Any] = {'admin_access': False} + if not user.has_billing_access: + context = {'admin_access': False} return render(request, 'corporate/billing.html', context=context) context = { diff --git a/frontend_tests/node_tests/billing_helpers.js b/frontend_tests/node_tests/billing_helpers.js index 585dfef462..c4dda448d4 100644 --- a/frontend_tests/node_tests/billing_helpers.js +++ b/frontend_tests/node_tests/billing_helpers.js @@ -139,6 +139,11 @@ run_test('create_ajax_request', () => { state.location_reload += 1; }; + window.location.replace = (reload_to) => { + state.location_reload += 1; + assert.equal(reload_to, "/billing"); + }; + success(); assert.equal(state.location_reload, 1); diff --git a/frontend_tests/node_tests/upgrade.js b/frontend_tests/node_tests/upgrade.js index 696dc9b435..a5abdea881 100644 --- a/frontend_tests/node_tests/upgrade.js +++ b/frontend_tests/node_tests/upgrade.js @@ -34,12 +34,22 @@ run_test("initialize", () => { assert.equal(page_name, "upgrade"); }; - helpers.create_ajax_request = (url, form_name, stripe_token) => { - assert.equal(url, "/json/billing/upgrade"); + helpers.create_ajax_request = (url, form_name, stripe_token, numeric_inputs, redirect_to) => { if (form_name === "autopay") { + assert.equal(url, "/json/billing/upgrade"); assert.equal(stripe_token, "stripe_add_card_token"); + assert.deepEqual(numeric_inputs, ["licenses"]); + assert.equal(redirect_to, undefined); } else if (form_name === "invoice") { + assert.equal(url, "/json/billing/upgrade"); assert.equal(stripe_token, undefined); + assert.deepEqual(numeric_inputs, ["licenses"]); + assert.equal(redirect_to, undefined); + } else if (form_name === "sponsorship") { + assert.equal(url, "/json/billing/sponsorship"); + assert.equal(stripe_token, undefined); + assert.equal(numeric_inputs, undefined); + assert.equal(redirect_to, "/"); } else { throw Error("Unhandled case"); } @@ -98,6 +108,7 @@ run_test("initialize", () => { const add_card_click_handler = $('#add-card-button').get_on_handler('click'); const invoice_click_handler = $('#invoice-button').get_on_handler('click'); + const request_sponsorship_click_handler = $('#sponsorship-button').get_on_handler('click'); helpers.is_valid_input = () => { return true; @@ -112,6 +123,8 @@ run_test("initialize", () => { add_card_click_handler(e); invoice_click_handler(e); + request_sponsorship_click_handler(e); + helpers.show_license_section = (section) => { assert.equal(section, "manual"); }; @@ -131,6 +144,26 @@ run_test("initialize", () => { assert.equal($("#autopay_monthly_price").text(), "6.40"); assert.equal($("#invoice_annual_price").text(), "64"); assert.equal($("#invoice_annual_price_per_month").text(), "5.34"); + + const organization_type_change_handler = $('select[name=organization-type]').get_on_handler('change'); + organization_type_change_handler.call({value: "open_source"}); + assert.equal($("#sponsorship-discount-details").text(), + "Open source projects are eligible for fully sponsored (free) Zulip Standard."); + organization_type_change_handler.call({value: "research"}); + assert.equal($("#sponsorship-discount-details").text(), + "Academic research organizations are eligible for fully sponsored (free) Zulip Standard."); + organization_type_change_handler.call({value: "event"}); + assert.equal($("#sponsorship-discount-details").text(), + "Events are eligible for fully sponsored (free) Zulip Standard."); + organization_type_change_handler.call({value: "education"}); + assert.equal($("#sponsorship-discount-details").text(), + "Education use is eligible for an 85%-100% discount."); + organization_type_change_handler.call({value: "non_profit"}); + assert.equal($("#sponsorship-discount-details").text(), + "Nonprofits are eligible for an 85%-100% discount."); + organization_type_change_handler.call({value: "other"}); + assert.equal($("#sponsorship-discount-details").text(), + "Your organization might be eligible for a discount or sponsorship."); }); run_test("autopay_form_fields", () => { diff --git a/static/js/billing/helpers.js b/static/js/billing/helpers.js index 3aaddffb1c..16cc336719 100644 --- a/static/js/billing/helpers.js +++ b/static/js/billing/helpers.js @@ -1,4 +1,4 @@ -exports.create_ajax_request = function (url, form_name, stripe_token = null, numeric_inputs = []) { +exports.create_ajax_request = function (url, form_name, stripe_token = null, numeric_inputs = [], redirect_to = "/billing") { const form = $("#" + form_name + "-form"); const form_loading_indicator = "#" + form_name + "_loading_indicator"; const form_input_section = "#" + form_name + "-input-section"; @@ -44,7 +44,7 @@ exports.create_ajax_request = function (url, form_name, stripe_token = null, num location.hash = ""; } } - location.reload(); + window.location.replace(redirect_to); }, error: function (xhr) { $(form_loading).hide(); @@ -75,6 +75,18 @@ exports.update_charged_amount = function (prices, schedule) { ); }; +exports.update_discount_details = function (organization_type) { + const discount_details = { + open_source: "Open source projects are eligible for fully sponsored (free) Zulip Standard.", + research: "Academic research organizations are eligible for fully sponsored (free) Zulip Standard.", + non_profit: "Nonprofits are eligible for an 85%-100% discount.", + event: "Events are eligible for fully sponsored (free) Zulip Standard.", + education: "Education use is eligible for an 85%-100% discount.", + other: "Your organization might be eligible for a discount or sponsorship.", + }; + $("#sponsorship-discount-details").text(discount_details[organization_type]); +}; + exports.show_license_section = function (license) { $("#license-automatic-section").hide(); $("#license-manual-section").hide(); diff --git a/static/js/billing/upgrade.js b/static/js/billing/upgrade.js index 0262d0e5e3..e6df0098bb 100644 --- a/static/js/billing/upgrade.js +++ b/static/js/billing/upgrade.js @@ -36,6 +36,11 @@ exports.initialize = () => { helpers.create_ajax_request("/json/billing/upgrade", "invoice", undefined, ["licenses"]); }); + $("#sponsorship-button").on("click", function (e) { + e.preventDefault(); + helpers.create_ajax_request("/json/billing/sponsorship", "sponsorship", undefined, undefined, "/"); + }); + const prices = {}; prices.annual = page_params.annual_price * (1 - page_params.percent_off / 100); prices.monthly = page_params.monthly_price * (1 - page_params.percent_off / 100); @@ -48,6 +53,10 @@ exports.initialize = () => { helpers.update_charged_amount(prices, this.value); }); + $('select[name=organization-type]').on("change", function () { + helpers.update_discount_details(this.value); + }); + $("#autopay_annual_price").text(helpers.format_money(prices.annual)); $("#autopay_annual_price_per_month").text(helpers.format_money(prices.annual / 12)); $("#autopay_monthly_price").text(helpers.format_money(prices.monthly)); diff --git a/static/styles/portico/activity.scss b/static/styles/portico/activity.scss index 8cef139e02..2bdcb34b10 100644 --- a/static/styles/portico/activity.scss +++ b/static/styles/portico/activity.scss @@ -79,6 +79,11 @@ tr.admin td:first-child { top: -25px; } +.sponsorship-pending-form { + position: relative; + top: -25px; +} + .scrub-realm-form { position: relative; top: -50px; diff --git a/static/styles/portico/billing.scss b/static/styles/portico/billing.scss index f0e9910931..0d1e4bde0f 100644 --- a/static/styles/portico/billing.scss +++ b/static/styles/portico/billing.scss @@ -177,6 +177,7 @@ display: none; } + #sponsorship-loading, #planchange-loading, #cardchange-loading, #invoice-loading, @@ -186,7 +187,7 @@ text-align: center; } - + #sponsorship-success, #planchange-success, #cardchange-success, #invoice-success, @@ -195,6 +196,7 @@ display: none; } + #sponsorship-error, #planchange-error, #cardchange-error, #invoice-error, @@ -224,6 +226,7 @@ stroke: hsl(0, 0%, 100%); } + #sponsorship_loading_indicator, #planchange_loading_indicator, #cardchange_loading_indicator, #invoice_loading_indicator, @@ -231,6 +234,7 @@ margin: 10px auto; } + #sponsorship_loading_indicator_box_container, #planchange_loading_indicator_box_container, #cardchange_loading_indicator_box_container, #invoice_loading_indicator_box_container, @@ -239,6 +243,7 @@ left: 50%; } + #sponsorship_loading_indicator_box, #planchange_loading_indicator_box, #cardchange_loading_indicator_box, #invoice_loading_indicator_box, @@ -250,6 +255,7 @@ border-radius: 6px; } + #sponsorship_loading_indicator .loading_indicator_text, #planchange_loading_indicator .loading_indicator_text, #cardchange_loading_indicator .loading_indicator_text, #invoice_loading_indicator .loading_indicator_text, diff --git a/templates/analytics/realm_details.html b/templates/analytics/realm_details.html index 41f6efc4ff..5945065bb7 100644 --- a/templates/analytics/realm_details.html +++ b/templates/analytics/realm_details.html @@ -30,6 +30,16 @@ +
+ Sponsorship pending:
+ {{ csrf_input }} + + + +
Discount (use 85 for nonprofits):
{{ csrf_input }} diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index 440b743624..7a3abe32f2 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -152,6 +152,9 @@ {% elif admin_access and not has_active_plan %}
+ {% if sponsorship_pending %} +

Your organization has requested sponsored or discounted hosting.

+ {% else %}

Your organization is on the Zulip Free plan.

@@ -163,6 +166,7 @@

+ {% endif %}
+ +
+
+
+ + + +
+ + + + +
+

+ + +
+
+ +
+
+
+ Request received! The page will now reload. +
+
+