mirror of https://github.com/zulip/zulip.git
stripe: Allow customer to switch license management type.
Fixes #28633 Added a button to switch license management type on billing page. Tested that the plan switch works correctly. Tested that when switching from manual to automatic license management, customer is only billed for billable users for the next billing cycle.
This commit is contained in:
parent
b7e02436b8
commit
790d5c44a1
|
@ -558,6 +558,7 @@ class UpdatePlanRequest:
|
|||
licenses: int | None
|
||||
licenses_at_next_renewal: int | None
|
||||
schedule: int | None
|
||||
toggle_license_management: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -2896,6 +2897,16 @@ class BillingSession(ABC):
|
|||
if last_ledger_entry is None:
|
||||
raise JsonableError(_("Unable to update the plan. The plan has ended."))
|
||||
|
||||
if update_plan_request.toggle_license_management:
|
||||
assert update_plan_request.status is None
|
||||
assert update_plan_request.licenses is None
|
||||
assert update_plan_request.licenses_at_next_renewal is None
|
||||
assert update_plan_request.schedule is None
|
||||
|
||||
plan.automanage_licenses = not plan.automanage_licenses
|
||||
plan.save(update_fields=["automanage_licenses"])
|
||||
return
|
||||
|
||||
status = update_plan_request.status
|
||||
if status is not None:
|
||||
if status == CustomerPlan.ACTIVE:
|
||||
|
|
|
@ -44,6 +44,7 @@ from corporate.lib.stripe import (
|
|||
SupportRequestError,
|
||||
SupportType,
|
||||
SupportViewRequest,
|
||||
UpdatePlanRequest,
|
||||
add_months,
|
||||
catch_stripe_errors,
|
||||
compute_plan_parameters,
|
||||
|
@ -1000,7 +1001,7 @@ class StripeTest(StripeTestCase):
|
|||
"Zulip Cloud Plus",
|
||||
str(self.seat_count),
|
||||
"Number of licenses",
|
||||
f"{ self.seat_count } (managed automatically)",
|
||||
f"{ self.seat_count }",
|
||||
"Your plan will automatically renew on",
|
||||
"January 2, 2013",
|
||||
f"${120 * self.seat_count}.00",
|
||||
|
@ -1270,7 +1271,7 @@ class StripeTest(StripeTestCase):
|
|||
"Zulip Cloud Standard",
|
||||
str(self.seat_count),
|
||||
"Number of licenses",
|
||||
f"{ self.seat_count } (managed automatically)",
|
||||
f"{ self.seat_count }",
|
||||
"Your plan will automatically renew on",
|
||||
"January 2, 2013",
|
||||
f"${80 * self.seat_count}.00",
|
||||
|
@ -1526,7 +1527,7 @@ class StripeTest(StripeTestCase):
|
|||
"Zulip Cloud Standard <i>(free trial)</i>",
|
||||
str(self.seat_count),
|
||||
"Number of licenses",
|
||||
f"{self.seat_count} (managed automatically)",
|
||||
f"{self.seat_count}",
|
||||
"Your plan will automatically renew on",
|
||||
"March 2, 2012",
|
||||
f"${80 * self.seat_count}.00",
|
||||
|
@ -5879,6 +5880,29 @@ class LicenseLedgerTest(StripeTestCase):
|
|||
],
|
||||
)
|
||||
|
||||
def test_toggle_license_management(self) -> None:
|
||||
self.local_upgrade(self.seat_count, True, CustomerPlan.BILLING_SCHEDULE_ANNUAL, True, False)
|
||||
plan = get_current_plan_by_realm(get_realm("zulip"))
|
||||
assert plan is not None
|
||||
self.assertEqual(plan.automanage_licenses, True)
|
||||
self.assertEqual(plan.licenses(), self.seat_count)
|
||||
self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count)
|
||||
billing_session = RealmBillingSession(user=None, realm=get_realm("zulip"))
|
||||
update_plan_request = UpdatePlanRequest(
|
||||
status=None,
|
||||
licenses=None,
|
||||
licenses_at_next_renewal=None,
|
||||
schedule=None,
|
||||
toggle_license_management=True,
|
||||
)
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.automanage_licenses, False)
|
||||
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.automanage_licenses, True)
|
||||
|
||||
|
||||
class InvoiceTest(StripeTestCase):
|
||||
def test_invoicing_status_is_started(self) -> None:
|
||||
|
@ -6954,7 +6978,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Business",
|
||||
"Number of licenses",
|
||||
f"{min_licenses} (managed automatically)",
|
||||
f"{min_licenses}",
|
||||
"January 2, 2013",
|
||||
"Your plan will automatically renew on",
|
||||
f"${80 * min_licenses:,.2f}",
|
||||
|
@ -6998,7 +7022,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Business",
|
||||
"Number of licenses",
|
||||
f"{latest_ledger.licenses} (managed automatically)",
|
||||
f"{latest_ledger.licenses}",
|
||||
"January 2, 2013",
|
||||
"Your plan will automatically renew on",
|
||||
f"${80 * latest_ledger.licenses:,.2f}",
|
||||
|
@ -7181,7 +7205,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
"Zulip Basic",
|
||||
"(free trial)",
|
||||
"Number of licenses",
|
||||
f"{realm_user_count} (managed automatically)",
|
||||
f"{realm_user_count}",
|
||||
"February 1, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -7225,7 +7249,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{latest_ledger.licenses} (managed automatically)",
|
||||
f"{latest_ledger.licenses}",
|
||||
"February 1, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -7389,7 +7413,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{realm_user_count} (managed automatically)",
|
||||
f"{realm_user_count}",
|
||||
"February 2, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -7433,7 +7457,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{latest_ledger.licenses} (managed automatically)",
|
||||
f"{latest_ledger.licenses}",
|
||||
"February 2, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -8292,7 +8316,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||
for substring in [
|
||||
"Zulip Business",
|
||||
"Number of licenses",
|
||||
f"{licenses} (managed automatically)",
|
||||
f"{licenses}",
|
||||
"Your plan will automatically renew on",
|
||||
"January 2, 2013",
|
||||
f"${80 * licenses:,.2f}",
|
||||
|
@ -8641,7 +8665,7 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||
for substring in [
|
||||
"Zulip Business",
|
||||
"Number of licenses",
|
||||
f"{25} (managed automatically)",
|
||||
f"{25}",
|
||||
"Your plan will automatically renew on",
|
||||
"January 2, 2013",
|
||||
f"${80 * 25:,.2f}",
|
||||
|
@ -9075,7 +9099,7 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||
"Zulip Basic",
|
||||
"(free trial)",
|
||||
"Number of licenses",
|
||||
f"{realm_user_count} (managed automatically)",
|
||||
f"{realm_user_count}",
|
||||
"February 1, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -9119,7 +9143,7 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{latest_ledger.licenses} (managed automatically)",
|
||||
f"{latest_ledger.licenses}",
|
||||
"February 1, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -9285,7 +9309,7 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{server_user_count} (managed automatically)",
|
||||
f"{server_user_count}",
|
||||
"February 2, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * server_user_count - flat_discount // 100 * 1:,.2f}",
|
||||
|
@ -9329,7 +9353,7 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||
for substring in [
|
||||
"Zulip Basic",
|
||||
"Number of licenses",
|
||||
f"{latest_ledger.licenses} (managed automatically)",
|
||||
f"{latest_ledger.licenses}",
|
||||
"February 2, 2012",
|
||||
"Your plan will automatically renew on",
|
||||
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
|
||||
|
|
|
@ -243,6 +243,7 @@ def update_plan(
|
|||
licenses: Json[int] | None = None,
|
||||
licenses_at_next_renewal: Json[int] | None = None,
|
||||
schedule: Json[int] | None = None,
|
||||
toggle_license_management: Json[bool] = False,
|
||||
) -> HttpResponse:
|
||||
from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest
|
||||
|
||||
|
@ -251,6 +252,7 @@ def update_plan(
|
|||
licenses=licenses,
|
||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
schedule=schedule,
|
||||
toggle_license_management=toggle_license_management,
|
||||
)
|
||||
billing_session = RealmBillingSession(user=user)
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
|
@ -271,6 +273,7 @@ def update_plan_for_remote_realm(
|
|||
licenses: Json[int] | None = None,
|
||||
licenses_at_next_renewal: Json[int] | None = None,
|
||||
schedule: Json[int] | None = None,
|
||||
toggle_license_management: Json[bool] = False,
|
||||
) -> HttpResponse:
|
||||
from corporate.lib.stripe import UpdatePlanRequest
|
||||
|
||||
|
@ -279,6 +282,7 @@ def update_plan_for_remote_realm(
|
|||
licenses=licenses,
|
||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
schedule=schedule,
|
||||
toggle_license_management=toggle_license_management,
|
||||
)
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
return json_success(request)
|
||||
|
@ -298,6 +302,7 @@ def update_plan_for_remote_server(
|
|||
licenses: Json[int] | None = None,
|
||||
licenses_at_next_renewal: Json[int] | None = None,
|
||||
schedule: Json[int] | None = None,
|
||||
toggle_license_management: Json[bool] = False,
|
||||
) -> HttpResponse:
|
||||
from corporate.lib.stripe import UpdatePlanRequest
|
||||
|
||||
|
@ -306,6 +311,7 @@ def update_plan_for_remote_server(
|
|||
licenses=licenses,
|
||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
schedule=schedule,
|
||||
toggle_license_management=toggle_license_management,
|
||||
)
|
||||
billing_session.do_update_plan(update_plan_request)
|
||||
return json_success(request)
|
||||
|
|
|
@ -123,7 +123,13 @@
|
|||
|
||||
</label>
|
||||
<div id="automatic-license-count" class="not-editable-realm-field">
|
||||
{{ licenses }} (managed automatically)
|
||||
{{ licenses }}
|
||||
<br />
|
||||
Licenses are managed
|
||||
<a href="https://zulip.com/help/self-hosted-billing#how-does-automatic-license-management-work">automatically</a>.
|
||||
You can
|
||||
<a class="toggle-license-management" type="button">switch</a>
|
||||
to manual license management.
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
@ -152,6 +158,13 @@
|
|||
</button>
|
||||
</div>
|
||||
<div id="current-license-change-error" class="alert alert-danger billing-page-error"></div>
|
||||
<div class="not-editable-realm-field billing-page-license-management-description">
|
||||
Licenses are managed
|
||||
<a href="https://zulip.com/help/self-hosted-billing#how-does-manual-license-management-work">manually</a>.
|
||||
You can
|
||||
<a class="toggle-license-management" type="button">switch</a>
|
||||
to automatic license management.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not (downgrade_at_end_of_cycle or downgrade_at_end_of_free_trial) %}
|
||||
|
@ -182,6 +195,10 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<form id="toggle-license-management-form">
|
||||
<input name="toggle_license_management" type="hidden" value="true" />
|
||||
</form>
|
||||
<div id="toggle-license-management-error" class="alert alert-danger"></div>
|
||||
<div class="input-box no-validation billing-page-field">
|
||||
<label for="billing-contact" class="inline-block label-title">Billing contact</label>
|
||||
<div id="billing-contact" class="not-editable-realm-field">
|
||||
|
@ -571,4 +588,41 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirm-toggle-license-management-modal" class="micromodal" aria-hidden="true">
|
||||
<div class="modal__overlay" tabindex="-1">
|
||||
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="dialog_title">
|
||||
<header class="modal__header">
|
||||
<h1 class="modal__title dialog_heading">
|
||||
Confirm switching to
|
||||
{% if automanage_licenses %}
|
||||
manual
|
||||
{% else %}
|
||||
automatic
|
||||
{% endif %}
|
||||
license management
|
||||
</h1>
|
||||
<button class="modal__close" aria-label="{{ _('Close modal') }}" data-micromodal-close></button>
|
||||
</header>
|
||||
<main class="modal__content">
|
||||
<p>
|
||||
Are you sure you want to switch to
|
||||
<a href="https://zulip.com/help/self-hosted-billing#how-does-automatic-license-management-work">
|
||||
{% if automanage_licenses %}
|
||||
manual
|
||||
{% else %}
|
||||
automatic
|
||||
{% endif %}
|
||||
license management
|
||||
</a>?
|
||||
</p>
|
||||
</main>
|
||||
<footer class="modal__footer">
|
||||
<button class="modal__btn dialog_exit_button" aria-label="{{ '(Close this dialog window)' }}" data-micromodal-close>{{ _('Never mind') }}</button>
|
||||
<button class="modal__btn dialog_submit_button">
|
||||
<span>{{ _('Confirm') }}</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -460,6 +460,24 @@ export function initialize(): void {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
$(".toggle-license-management").on("click", (e) => {
|
||||
e.preventDefault();
|
||||
portico_modals.open("confirm-toggle-license-management-modal");
|
||||
});
|
||||
|
||||
$("#confirm-toggle-license-management-modal").on("click", ".dialog_submit_button", (e) => {
|
||||
helpers.create_ajax_request(
|
||||
`/json${billing_base_url}/billing/plan`,
|
||||
"toggle-license-management",
|
||||
[],
|
||||
"PATCH",
|
||||
() => {
|
||||
window.location.replace(`${billing_base_url}/billing/`);
|
||||
},
|
||||
);
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
|
|
|
@ -748,3 +748,11 @@ input[name="licenses"] {
|
|||
.flat-discount-separator {
|
||||
border-bottom: 1px solid hsl(0deg 0% 0%);
|
||||
}
|
||||
|
||||
.toggle-license-management {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#billing-page-details .billing-page-license-management-description {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue