mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	billing: Add sponsorship request form to the billing page.
Previously this was only available on the upgrade page - meaning an organization that already bought a plan wouldn't be able to request a sponsorship to get a discount or such, even if qualified.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							cf55e66c74
						
					
				
				
					commit
					684430faa2
				
			@@ -21,6 +21,7 @@ from typing import (
 | 
			
		||||
    Tuple,
 | 
			
		||||
    TypeVar,
 | 
			
		||||
)
 | 
			
		||||
from unittest import mock
 | 
			
		||||
from unittest.mock import Mock, patch
 | 
			
		||||
 | 
			
		||||
import orjson
 | 
			
		||||
@@ -3828,6 +3829,47 @@ 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 <strong>Zulip Cloud Standard</strong>."], result
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assert_in_success_response(["Request sponsorship"], result)
 | 
			
		||||
 | 
			
		||||
    def test_update_billing_method_of_current_plan(self) -> None:
 | 
			
		||||
        realm = get_realm("zulip")
 | 
			
		||||
        customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ from zerver.lib.exceptions import JsonableError
 | 
			
		||||
from zerver.lib.request import REQ, has_request_variables
 | 
			
		||||
from zerver.lib.response import json_success
 | 
			
		||||
from zerver.lib.validator import check_bool, check_int, check_int_in
 | 
			
		||||
from zerver.models import UserProfile
 | 
			
		||||
from zerver.models import Realm, UserProfile
 | 
			
		||||
 | 
			
		||||
billing_logger = logging.getLogger("corporate.stripe")
 | 
			
		||||
 | 
			
		||||
@@ -58,6 +58,23 @@ def payment_method_string(stripe_customer: stripe.Customer) -> str:
 | 
			
		||||
    )  # nocoverage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_sponsorship_info_to_context(context: Dict[str, Any], user_profile: UserProfile) -> None:
 | 
			
		||||
    def key_helper(d: Any) -> int:
 | 
			
		||||
        return d[1]["display_order"]
 | 
			
		||||
 | 
			
		||||
    context.update(
 | 
			
		||||
        realm_org_type=user_profile.realm.org_type,
 | 
			
		||||
        sorted_org_types=sorted(
 | 
			
		||||
            (
 | 
			
		||||
                [org_type_name, org_type]
 | 
			
		||||
                for (org_type_name, org_type) in Realm.ORG_TYPES.items()
 | 
			
		||||
                if not org_type.get("hidden")
 | 
			
		||||
            ),
 | 
			
		||||
            key=key_helper,
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@zulip_login_required
 | 
			
		||||
@has_request_variables
 | 
			
		||||
def billing_home(
 | 
			
		||||
@@ -145,6 +162,7 @@ def billing_home(
 | 
			
		||||
                CustomerPlan=CustomerPlan,
 | 
			
		||||
                onboarding=onboarding,
 | 
			
		||||
            )
 | 
			
		||||
            add_sponsorship_info_to_context(context, user)
 | 
			
		||||
 | 
			
		||||
    return render(request, "corporate/billing.html", context=context)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,14 +36,14 @@ from corporate.models import (
 | 
			
		||||
    get_current_plan_by_customer,
 | 
			
		||||
    get_customer_by_realm,
 | 
			
		||||
)
 | 
			
		||||
from corporate.views.billing_page import billing_home
 | 
			
		||||
from corporate.views.billing_page import add_sponsorship_info_to_context, 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
 | 
			
		||||
from zerver.lib.response import json_success
 | 
			
		||||
from zerver.lib.send_email import FromAddress, send_email
 | 
			
		||||
from zerver.lib.validator import check_bool, check_int, check_string_in
 | 
			
		||||
from zerver.models import Realm, UserProfile, get_org_type_display_name
 | 
			
		||||
from zerver.models import UserProfile, get_org_type_display_name
 | 
			
		||||
 | 
			
		||||
billing_logger = logging.getLogger("corporate.stripe")
 | 
			
		||||
 | 
			
		||||
@@ -278,17 +278,10 @@ def initial_upgrade(
 | 
			
		||||
            "percent_off": float(percent_off),
 | 
			
		||||
            "demo_organization_scheduled_deletion_date": user.realm.demo_organization_scheduled_deletion_date,
 | 
			
		||||
        },
 | 
			
		||||
        "realm_org_type": user.realm.org_type,
 | 
			
		||||
        "sorted_org_types": sorted(
 | 
			
		||||
            (
 | 
			
		||||
                [org_type_name, org_type]
 | 
			
		||||
                for (org_type_name, org_type) in Realm.ORG_TYPES.items()
 | 
			
		||||
                if not org_type.get("hidden")
 | 
			
		||||
            ),
 | 
			
		||||
            key=lambda d: d[1]["display_order"],
 | 
			
		||||
        ),
 | 
			
		||||
        "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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@
 | 
			
		||||
                    <li class="active"><a data-toggle="tab" href="#overview">Overview</a></li>
 | 
			
		||||
                    <li><a data-toggle="tab" href="#payment-method">Payment method</a></li>
 | 
			
		||||
                    <li><a data-toggle="tab" href="#settings">Settings</a></li>
 | 
			
		||||
                    <li><a data-toggle="tab" href="#sponsorship">💚 Request sponsorship</a></li>
 | 
			
		||||
                </ul>
 | 
			
		||||
 | 
			
		||||
                <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />
 | 
			
		||||
@@ -170,6 +171,8 @@
 | 
			
		||||
                        <div class="tab-pane" id="loading">
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {% include "corporate/sponsorship.html" %}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div id="goto-zulip-organization-link">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								templates/corporate/sponsorship.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								templates/corporate/sponsorship.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
<div class="tab-pane" id="sponsorship">
 | 
			
		||||
    <div id="sponsorship-error" class="alert alert-danger"></div>
 | 
			
		||||
    <div id="sponsorship-input-section">
 | 
			
		||||
        <form id="sponsorship-form" method="post">
 | 
			
		||||
            <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />
 | 
			
		||||
            <label>
 | 
			
		||||
                <h4>Organization type</h4>
 | 
			
		||||
            </label>
 | 
			
		||||
            <select name="organization-type" class="bootstrap-focus-style">
 | 
			
		||||
                {% for org_type in sorted_org_types %}
 | 
			
		||||
                    {% if not org_type[1].hidden %}
 | 
			
		||||
                    <option data-string-value="{{ org_type[0] }}"
 | 
			
		||||
                      {% if org_type[1].id == realm_org_type %}selected{% endif %}
 | 
			
		||||
                      value="{{ org_type[1].id }}">
 | 
			
		||||
                        {{ _(org_type[1].name) }}
 | 
			
		||||
                    </option>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </select>
 | 
			
		||||
            <br />
 | 
			
		||||
            <label>
 | 
			
		||||
                <h4>Organization website</h4>
 | 
			
		||||
            </label>
 | 
			
		||||
            <input name="website" type="text" class="input-large" placeholder="{{ _('Leave blank if your organization does not have a website.') }}"/>
 | 
			
		||||
            <label>
 | 
			
		||||
                <h4>Describe your organization briefly</h4>
 | 
			
		||||
            </label>
 | 
			
		||||
            <textarea name="description" cols="100" rows="5" required></textarea>
 | 
			
		||||
            <br />
 | 
			
		||||
            <p id="sponsorship-discount-details"></p>
 | 
			
		||||
            <!-- Disabled buttons do not fire any events, so we need a container div that isn't disabled for tippyjs to work -->
 | 
			
		||||
            <div class="upgrade-button-container" {% if is_demo_organization %}data-tippy-content="{% trans %}Convert demo organization before upgrading.{% endtrans %}"{% endif %}>
 | 
			
		||||
                <button type="submit" id="sponsorship-button" class="stripe-button-el invoice-button" {% if is_demo_organization %}disabled{% endif %}>
 | 
			
		||||
                    Submit
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="sponsorship-loading">
 | 
			
		||||
        <div class="zulip-loading-logo">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12">
 | 
			
		||||
                <circle cx="386.56" cy="386.56" r="386.56"/>
 | 
			
		||||
                <path d="M566.66 527.25c0 33.03-24.23 60.05-53.84 60.05H260.29c-29.61 0-53.84-27.02-53.84-60.05 0-20.22 9.09-38.2 22.93-49.09l134.37-120c2.5-2.14 5.74 1.31 3.94 4.19l-49.29 98.69c-1.38 2.76.41 6.16 3.25 6.16h191.18c29.61 0 53.83 27.03 53.83 60.05zm0-281.39c0 20.22-9.09 38.2-22.93 49.09l-134.37 120c-2.5 2.14-5.74-1.31-3.94-4.19l49.29-98.69c1.38-2.76-.41-6.16-3.25-6.16H260.29c-29.61 0-53.84-27.02-53.84-60.05s24.23-60.05 53.84-60.05h252.54c29.61 0 53.83 27.02 53.83 60.05z"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="sponsorship_loading_indicator"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="sponsorship-success" class="alert alert-info">
 | 
			
		||||
        Request received! The page will now reload.
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -241,56 +241,7 @@
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="tab-pane" id="sponsorship">
 | 
			
		||||
                        <div id="sponsorship-error" class="alert alert-danger"></div>
 | 
			
		||||
                        <div id="sponsorship-input-section">
 | 
			
		||||
                            <form id="sponsorship-form" method="post">
 | 
			
		||||
                                <label>
 | 
			
		||||
                                    <h4>Organization type</h4>
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <select name="organization-type" class="bootstrap-focus-style">
 | 
			
		||||
                                    {% for org_type in sorted_org_types %}
 | 
			
		||||
                                        {% if not org_type[1].hidden %}
 | 
			
		||||
                                        <option data-string-value="{{ org_type[0] }}"
 | 
			
		||||
                                          {% if org_type[1].id == realm_org_type %}selected{% endif %}
 | 
			
		||||
                                          value="{{ org_type[1].id }}">
 | 
			
		||||
                                            {{ _(org_type[1].name) }}
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                </select>
 | 
			
		||||
                                <br />
 | 
			
		||||
                                <label>
 | 
			
		||||
                                    <h4>Organization website</h4>
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <input name="website" type="text" class="input-large" placeholder="{{ _('Leave blank if your organization does not have a website.') }}"/>
 | 
			
		||||
                                <label>
 | 
			
		||||
                                    <h4>Describe your organization briefly</h4>
 | 
			
		||||
                                </label>
 | 
			
		||||
                                <textarea name="description" cols="100" rows="5" required></textarea>
 | 
			
		||||
                                <br />
 | 
			
		||||
                                <p id="sponsorship-discount-details"></p>
 | 
			
		||||
                                <!-- Disabled buttons do not fire any events, so we need a container div that isn't disabled for tippyjs to work -->
 | 
			
		||||
                                <div class="upgrade-button-container" {% if is_demo_organization %}data-tippy-content="{% trans %}Convert demo organization before upgrading.{% endtrans %}"{% endif %}>
 | 
			
		||||
                                    <button type="submit" id="sponsorship-button" class="stripe-button-el invoice-button" {% if is_demo_organization %}disabled{% endif %}>
 | 
			
		||||
                                        Submit
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div id="sponsorship-loading">
 | 
			
		||||
                            <div class="zulip-loading-logo">
 | 
			
		||||
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12">
 | 
			
		||||
                                    <circle cx="386.56" cy="386.56" r="386.56"/>
 | 
			
		||||
                                    <path d="M566.66 527.25c0 33.03-24.23 60.05-53.84 60.05H260.29c-29.61 0-53.84-27.02-53.84-60.05 0-20.22 9.09-38.2 22.93-49.09l134.37-120c2.5-2.14 5.74 1.31 3.94 4.19l-49.29 98.69c-1.38 2.76.41 6.16 3.25 6.16h191.18c29.61 0 53.83 27.03 53.83 60.05zm0-281.39c0 20.22-9.09 38.2-22.93 49.09l-134.37 120c-2.5 2.14-5.74-1.31-3.94-4.19l49.29-98.69c1.38-2.76-.41-6.16-3.25-6.16H260.29c-29.61 0-53.84-27.02-53.84-60.05s24.23-60.05 53.84-60.05h252.54c29.61 0 53.83 27.02 53.83 60.05z"/>
 | 
			
		||||
                                </svg>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div id="sponsorship_loading_indicator"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div id="sponsorship-success" class="alert alert-info">
 | 
			
		||||
                            Request received! The page will now reload.
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% include "corporate/sponsorship.html" %}
 | 
			
		||||
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="support-link">
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ export function create_update_license_request() {
 | 
			
		||||
 | 
			
		||||
export function initialize() {
 | 
			
		||||
    helpers.set_tab("billing");
 | 
			
		||||
    helpers.set_sponsorship_form();
 | 
			
		||||
 | 
			
		||||
    $("#update-card-button").on("click", (e) => {
 | 
			
		||||
        const success_callback = (response) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -136,6 +136,18 @@ export function set_tab(page) {
 | 
			
		||||
    window.addEventListener("hashchange", handle_hashchange);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function set_sponsorship_form() {
 | 
			
		||||
    $("#sponsorship-button").on("click", (e) => {
 | 
			
		||||
        if (!is_valid_input($("#sponsorship-form"))) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        create_ajax_request("/json/billing/sponsorship", "sponsorship", [], "POST", () =>
 | 
			
		||||
            window.location.replace("/"),
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function is_valid_input(elem) {
 | 
			
		||||
    return elem[0].checkValidity();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import * as helpers from "./helpers";
 | 
			
		||||
 | 
			
		||||
export const initialize = () => {
 | 
			
		||||
    helpers.set_tab("upgrade");
 | 
			
		||||
    helpers.set_sponsorship_form();
 | 
			
		||||
    $("#add-card-button").on("click", (e) => {
 | 
			
		||||
        const license_management = $("input[type=radio][name=license_management]:checked").val();
 | 
			
		||||
        if (
 | 
			
		||||
@@ -36,16 +37,6 @@ export const initialize = () => {
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $("#sponsorship-button").on("click", (e) => {
 | 
			
		||||
        if (!helpers.is_valid_input($("#sponsorship-form"))) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        helpers.create_ajax_request("/json/billing/sponsorship", "sponsorship", [], "POST", () =>
 | 
			
		||||
            window.location.replace("/"),
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ const location = set_global("location", {});
 | 
			
		||||
 | 
			
		||||
const helpers = mock_esm("../src/billing/helpers", {
 | 
			
		||||
    set_tab() {},
 | 
			
		||||
    set_sponsorship_form() {},
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
zrequire("billing/billing");
 | 
			
		||||
@@ -30,8 +31,13 @@ run_test("initialize", ({override}) => {
 | 
			
		||||
        assert.equal(page_name, "billing");
 | 
			
		||||
        set_tab_called = true;
 | 
			
		||||
    });
 | 
			
		||||
    let set_sponsorship_form_called = false;
 | 
			
		||||
    override(helpers, "set_sponsorship_form", () => {
 | 
			
		||||
        set_sponsorship_form_called = true;
 | 
			
		||||
    });
 | 
			
		||||
    $.get_initialize_function()();
 | 
			
		||||
    assert.ok(set_tab_called);
 | 
			
		||||
    assert.ok(set_sponsorship_form_called);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
run_test("card_update", ({override}) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user