billing: Fix license counting in exempt_from_license_number_check case.

Fixes two bugs involving organization with
exempt_from_license_number_check enabled:
1. If the organization had e.g. 100 users and upgraded their plan,
   specifying 50 licenses, the generated LicenseLedger and thus the
   corresponding invoice was still for 100 users.
2. The organization was unable to use the billing/plan endpoint (update
   plan endpoint) to make their number of licenses less than the current
   number of users.

Organizations with exempt_from_license_number_check are supposed to be
able to declare whatever license number they want, as this attribute
allows having pricing schemes where an organization only pays us for a
subset of their users.
This commit is contained in:
Mateusz Mandera 2023-09-26 01:57:58 +02:00 committed by Tim Abbott
parent 1de3a9f4a3
commit 07e99eace9
2 changed files with 66 additions and 5 deletions

View File

@ -700,9 +700,12 @@ def process_initial_upgrade(
# TODO: The correctness of this relies on user creation, deactivation, etc being # TODO: The correctness of this relies on user creation, deactivation, etc being
# in a transaction.atomic() with the relevant RealmAuditLog entries # in a transaction.atomic() with the relevant RealmAuditLog entries
with transaction.atomic(): with transaction.atomic():
# billed_licenses can greater than licenses if users are added between the start of if customer.exempt_from_license_number_check:
# this function (process_initial_upgrade) and now billed_licenses = licenses
billed_licenses = max(get_latest_seat_count(realm), licenses) else:
# billed_licenses can be greater than licenses if users are added between the start of
# this function (process_initial_upgrade) and now
billed_licenses = max(get_latest_seat_count(realm), licenses)
plan_params = { plan_params = {
"automanage_licenses": automanage_licenses, "automanage_licenses": automanage_licenses,
"charge_automatically": charge_automatically, "charge_automatically": charge_automatically,
@ -777,14 +780,16 @@ def update_license_ledger_for_manual_plan(
) -> None: ) -> None:
if licenses is not None: if licenses is not None:
assert plan.customer.realm is not None assert plan.customer.realm is not None
assert get_latest_seat_count(plan.customer.realm) <= licenses if not plan.customer.exempt_from_license_number_check:
assert get_latest_seat_count(plan.customer.realm) <= licenses
assert licenses > plan.licenses() assert licenses > plan.licenses()
LicenseLedger.objects.create( LicenseLedger.objects.create(
plan=plan, event_time=event_time, licenses=licenses, licenses_at_next_renewal=licenses plan=plan, event_time=event_time, licenses=licenses, licenses_at_next_renewal=licenses
) )
elif licenses_at_next_renewal is not None: elif licenses_at_next_renewal is not None:
assert plan.customer.realm is not None assert plan.customer.realm is not None
assert get_latest_seat_count(plan.customer.realm) <= licenses_at_next_renewal if not plan.customer.exempt_from_license_number_check:
assert get_latest_seat_count(plan.customer.realm) <= licenses_at_next_renewal
LicenseLedger.objects.create( LicenseLedger.objects.create(
plan=plan, plan=plan,
event_time=event_time, event_time=event_time,

View File

@ -3397,6 +3397,62 @@ class StripeTest(StripeTestCase):
for key, value in line_item_params.items(): for key, value in line_item_params.items():
self.assertEqual(renewal_item.get(key), value) self.assertEqual(renewal_item.get(key), value)
def test_update_licenses_of_manual_plan_from_billing_page_exempt_from_license_number_check(
self,
) -> None:
"""
Verifies that an organization exempt from the license number check can reduce their number
of licenses.
"""
user = self.example_user("hamlet")
self.login_user(user)
customer = Customer.objects.get_or_create(realm=user.realm)[0]
customer.exempt_from_license_number_check = True
customer.save()
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
self.local_upgrade(100, False, CustomerPlan.ANNUAL, True, False)
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
result = self.client_patch(
"/json/billing/plan",
{"licenses_at_next_renewal": get_latest_seat_count(user.realm) - 2},
)
self.assert_json_success(result)
latest_license_ledger = LicenseLedger.objects.last()
assert latest_license_ledger is not None
self.assertEqual(
latest_license_ledger.licenses_at_next_renewal, get_latest_seat_count(user.realm) - 2
)
def test_upgrade_exempt_from_license_number_check_realm_less_licenses_than_seat_count(
self,
) -> None:
"""
Verifies that an organization exempt from the license number check can upgrade their plan,
specifying a number of licenses less than their current number of licenses and be charged
for the number of licenses specified. Tests against a former bug, where the organization
was charged for the current seat count, despite specifying a lower number of licenses.
"""
user = self.example_user("hamlet")
self.login_user(user)
customer = Customer.objects.get_or_create(realm=user.realm)[0]
customer.exempt_from_license_number_check = True
customer.save()
reduced_seat_count = get_latest_seat_count(user.realm) - 2
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
self.local_upgrade(reduced_seat_count, False, CustomerPlan.ANNUAL, True, False)
latest_license_ledger = LicenseLedger.objects.last()
assert latest_license_ledger is not None
self.assertEqual(latest_license_ledger.licenses_at_next_renewal, reduced_seat_count)
self.assertEqual(latest_license_ledger.licenses, reduced_seat_count)
def test_update_licenses_of_automatic_plan_from_billing_page(self) -> None: def test_update_licenses_of_automatic_plan_from_billing_page(self) -> None:
user = self.example_user("hamlet") user = self.example_user("hamlet")
self.login_user(user) self.login_user(user)