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.
This commit is contained in:
Mateusz Mandera 2023-04-10 21:48:52 +02:00 committed by Tim Abbott
parent be208f73f7
commit ef42065cec
6 changed files with 47 additions and 11 deletions

View File

@ -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)
)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -151,8 +151,8 @@
{% endif %}
</p>
<h4>Number of licenses (minimum {{ seat_count }})</h4>
<input type="number" name="licenses" min="{{ seat_count }}" autocomplete="off" id="manual_license_count" required/><br />
<h4>Number of licenses {% if not exempt_from_license_number_check %}(minimum {{ seat_count }}){% endif %}</h4>
<input type="number" name="licenses" {% if not exempt_from_license_number_check %}min="{{ seat_count }}"{% endif %} autocomplete="off" id="manual_license_count" required/><br />
</div>
<!-- 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 %}>

View File

@ -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]",