From ef42065cec63086d0bc8394c891a6850a8f98968 Mon Sep 17 00:00:00 2001
From: Mateusz Mandera
Date: Mon, 10 Apr 2023 21:48:52 +0200
Subject: [PATCH] billing: Allow exempt_from_license_number_check any number of
licenses.
exempt_from_license_number_check was initially added allowing
organizations with it enabled to invite new users above their number of
licenses.
However, an organization with this permission enabled,
cannot upgrade if they weren't on a plan already - because when choosing
Manual license management, you cannot enter a number of licenses lower
than the current seat count. However, an organization like that probably
already has some users that they get free of charge - and thus they need
to be able to enter a lower number of licenses in order to upgrade.
---
corporate/lib/stripe.py | 9 +++++++--
corporate/tests/test_stripe.py | 8 ++++++++
corporate/views/billing_page.py | 8 +++++++-
corporate/views/upgrade.py | 25 +++++++++++++++++++++++--
templates/corporate/upgrade.html | 4 ++--
web/tests/upgrade.test.js | 4 ----
6 files changed, 47 insertions(+), 11 deletions(-)
diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py
index 97a6a4e58d..c2a65d6092 100644
--- a/corporate/lib/stripe.py
+++ b/corporate/lib/stripe.py
@@ -95,14 +95,19 @@ def unsign_string(signed_string: str, salt: str) -> str:
return signer.unsign(signed_string)
-def validate_licenses(charge_automatically: bool, licenses: Optional[int], seat_count: int) -> None:
+def validate_licenses(
+ charge_automatically: bool,
+ licenses: Optional[int],
+ seat_count: int,
+ exempt_from_license_number_check: bool,
+) -> None:
min_licenses = seat_count
max_licenses = None
if not charge_automatically:
min_licenses = max(seat_count, MIN_INVOICED_LICENSES)
max_licenses = MAX_INVOICED_LICENSES
- if licenses is None or licenses < min_licenses:
+ if licenses is None or (not exempt_from_license_number_check and licenses < min_licenses):
raise BillingError(
"not enough licenses", _("You must invoice for at least {} users.").format(min_licenses)
)
diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py
index 80658942a5..34d5c153f8 100644
--- a/corporate/tests/test_stripe.py
+++ b/corporate/tests/test_stripe.py
@@ -2061,6 +2061,14 @@ class StripeTest(StripeTestCase):
# Invoice
check_success(True, MAX_INVOICED_LICENSES)
+ # By default, an organization on a "Pay by card" plan with Manual license
+ # management cannot purchase less licenses than the current seat count.
+ # If exempt_from_license_number_check is enabled, they should be able to though.
+ customer = Customer.objects.get_or_create(realm=hamlet.realm)[0]
+ customer.exempt_from_license_number_check = True
+ customer.save()
+ check_success(False, self.seat_count - 1, {"license_management": "manual"})
+
def test_upgrade_with_uncaught_exception(self) -> None:
hamlet = self.example_user("hamlet")
self.login_user(hamlet)
diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py
index 214928e348..2cf85d1f78 100644
--- a/corporate/views/billing_page.py
+++ b/corporate/views/billing_page.py
@@ -237,7 +237,12 @@ def update_plan(
licenses=licenses
)
)
- validate_licenses(plan.charge_automatically, licenses, get_latest_seat_count(user.realm))
+ validate_licenses(
+ plan.charge_automatically,
+ licenses,
+ get_latest_seat_count(user.realm),
+ plan.customer.exempt_from_license_number_check,
+ )
update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses)
return json_success(request)
@@ -258,6 +263,7 @@ def update_plan(
plan.charge_automatically,
licenses_at_next_renewal,
get_latest_seat_count(user.realm),
+ plan.customer.exempt_from_license_number_check,
)
update_license_ledger_for_manual_plan(
plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal
diff --git a/corporate/views/upgrade.py b/corporate/views/upgrade.py
index 958930f2da..3f446995a0 100644
--- a/corporate/views/upgrade.py
+++ b/corporate/views/upgrade.py
@@ -65,6 +65,7 @@ def check_upgrade_parameters(
license_management: Optional[str],
licenses: Optional[int],
seat_count: int,
+ exempt_from_license_number_check: bool,
) -> None:
if billing_modality not in VALID_BILLING_MODALITY_VALUES: # nocoverage
raise BillingError("unknown billing_modality", "")
@@ -72,7 +73,12 @@ def check_upgrade_parameters(
raise BillingError("unknown schedule")
if license_management not in VALID_LICENSE_MANAGEMENT_VALUES: # nocoverage
raise BillingError("unknown license_management")
- validate_licenses(billing_modality == "charge_automatically", licenses, seat_count)
+ validate_licenses(
+ billing_modality == "charge_automatically",
+ licenses,
+ seat_count,
+ exempt_from_license_number_check,
+ )
def setup_upgrade_checkout_session_and_payment_intent(
@@ -171,8 +177,18 @@ def upgrade(
if billing_modality == "send_invoice":
schedule = "annual"
license_management = "manual"
+
+ customer = get_customer_by_realm(user.realm)
+ exempt_from_license_number_check = (
+ customer is not None and customer.exempt_from_license_number_check
+ )
check_upgrade_parameters(
- billing_modality, schedule, license_management, licenses, seat_count
+ billing_modality,
+ schedule,
+ license_management,
+ licenses,
+ seat_count,
+ exempt_from_license_number_check,
)
assert licenses is not None and license_management is not None
automanage_licenses = license_management == "automatic"
@@ -258,6 +274,10 @@ def initial_upgrade(
if customer is not None and customer.default_discount is not None:
percent_off = customer.default_discount
+ exempt_from_license_number_check = (
+ customer is not None and customer.exempt_from_license_number_check
+ )
+
seat_count = get_latest_seat_count(user.realm)
signed_seat_count, salt = sign_string(str(seat_count))
context: Dict[str, Any] = {
@@ -268,6 +288,7 @@ def initial_upgrade(
"salt": salt,
"min_invoiced_licenses": max(seat_count, MIN_INVOICED_LICENSES),
"default_invoice_days_until_due": DEFAULT_INVOICE_DAYS_UNTIL_DUE,
+ "exempt_from_license_number_check": exempt_from_license_number_check,
"plan": "Zulip Cloud Standard",
"free_trial_days": settings.FREE_TRIAL_DAYS,
"onboarding": onboarding,
diff --git a/templates/corporate/upgrade.html b/templates/corporate/upgrade.html
index de91bb249a..5d51bfa1e6 100644
--- a/templates/corporate/upgrade.html
+++ b/templates/corporate/upgrade.html
@@ -151,8 +151,8 @@
{% endif %}
- Number of licenses (minimum {{ seat_count }})
-
+ Number of licenses {% if not exempt_from_license_number_check %}(minimum {{ seat_count }}){% endif %}
+
diff --git a/web/tests/upgrade.test.js b/web/tests/upgrade.test.js
index f4a8e28a3f..b345665ed2 100644
--- a/web/tests/upgrade.test.js
+++ b/web/tests/upgrade.test.js
@@ -187,10 +187,6 @@ run_test("autopay_form_fields", () => {
document.querySelector("#autopay-form #automatic_license_count").value,
"{{ seat_count }}",
);
- assert.equal(
- document.querySelector("#autopay-form #manual_license_count").min,
- "{{ seat_count }}",
- );
const license_options = document.querySelectorAll(
"#autopay-form input[type=radio][name=license_management]",