mirror of https://github.com/zulip/zulip.git
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:
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">
|
||||
|
|
|
@ -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}) => {
|
||||
|
|
Loading…
Reference in New Issue