billing: Create LicenseLedger entry for guest user role changes.

As guest users are charged at a different rate on Zulip Cloud, it
makes sense to track these changes in our LicenseLedger table so
that we can have an accurate record of the use of licenses that
a realm has purchased.
This commit is contained in:
Lauryn Menard 2024-09-04 21:27:08 +02:00 committed by Tim Abbott
parent ac13546fa2
commit 4b954a42ef
27 changed files with 66 additions and 1 deletions

View File

@ -86,7 +86,7 @@ from zerver.actions.create_user import (
do_reactivate_user, do_reactivate_user,
) )
from zerver.actions.realm_settings import do_deactivate_realm, do_reactivate_realm from zerver.actions.realm_settings import do_deactivate_realm, do_reactivate_realm
from zerver.actions.users import do_deactivate_user from zerver.actions.users import do_change_user_role, do_deactivate_user
from zerver.lib.remote_server import send_server_data_to_push_bouncer from zerver.lib.remote_server import send_server_data_to_push_bouncer
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import activate_push_notification_service from zerver.lib.test_helpers import activate_push_notification_service
@ -5713,6 +5713,19 @@ class LicenseLedgerTest(StripeTestCase):
do_reactivate_user(user, acting_user=None) do_reactivate_user(user, acting_user=None)
# Not a proper use of do_activate_mirror_dummy_user, but fine for this test # Not a proper use of do_activate_mirror_dummy_user, but fine for this test
do_activate_mirror_dummy_user(user, acting_user=None) do_activate_mirror_dummy_user(user, acting_user=None)
# Add a guest user
guest = do_create_user(
"guest_email",
"guest_password",
get_realm("zulip"),
"guest_name",
role=UserProfile.ROLE_GUEST,
acting_user=None,
)
# Change guest user role to member
do_change_user_role(guest, UserProfile.ROLE_MEMBER, acting_user=None)
# Change again to moderator, no LicenseLedger created
do_change_user_role(guest, UserProfile.ROLE_MODERATOR, acting_user=None)
ledger_entries = list( ledger_entries = list(
LicenseLedger.objects.values_list( LicenseLedger.objects.values_list(
"is_renewal", "licenses", "licenses_at_next_renewal" "is_renewal", "licenses", "licenses_at_next_renewal"
@ -5726,6 +5739,8 @@ class LicenseLedgerTest(StripeTestCase):
(False, self.seat_count + 1, self.seat_count), (False, self.seat_count + 1, self.seat_count),
(False, self.seat_count + 1, self.seat_count + 1), (False, self.seat_count + 1, self.seat_count + 1),
(False, self.seat_count + 1, self.seat_count + 1), (False, self.seat_count + 1, self.seat_count + 1),
(False, self.seat_count + 1, self.seat_count + 1),
(False, self.seat_count + 2, self.seat_count + 2),
], ],
) )
@ -5897,6 +5912,52 @@ class InvoiceTest(StripeTestCase):
assert plan is not None assert plan is not None
self.assertEqual(plan.next_invoice_date, self.next_month + timedelta(days=29)) self.assertEqual(plan.next_invoice_date, self.next_month + timedelta(days=29))
@mock_stripe()
def test_invoice_for_additional_license(self, *mocks: Mock) -> None:
user = self.example_user("hamlet")
self.login_user(user)
with time_machine.travel(self.now, tick=False):
self.add_card_and_upgrade(user)
plan = CustomerPlan.objects.first()
assert plan is not None
self.assertEqual(plan.next_invoice_date, self.next_month)
assert plan.customer.realm is not None
realm = plan.customer.realm
# Adding a guest user and then changing their role to member
# should invoice for a pro-rated license at the next invoice
# date on a plan with annual billing.
with time_machine.travel(self.now + timedelta(days=5), tick=False):
user = do_create_user(
"email",
"password",
realm,
"name",
role=UserProfile.ROLE_GUEST,
acting_user=None,
)
with time_machine.travel(self.now + timedelta(days=10), tick=False):
do_change_user_role(user, UserProfile.ROLE_MEMBER, acting_user=None)
billing_session = RealmBillingSession(realm=realm)
billing_session.invoice_plan(plan, self.next_month)
plan = CustomerPlan.objects.first()
assert plan is not None
self.assertEqual(plan.next_invoice_date, self.next_month + timedelta(days=29))
stripe_customer_id = plan.customer.stripe_customer_id
assert stripe_customer_id is not None
[invoice0, invoice1] = iter(stripe.Invoice.list(customer=stripe_customer_id))
self.assertIsNotNone(invoice0.status_transitions.finalized_at)
[item0] = iter(invoice0.lines)
line_item_params = {
"amount": int(8000 * (1 - ((366 - 356) / 366)) + 0.5),
"description": "Additional license (Jan 12, 2012 - Jan 2, 2013)",
"quantity": 1,
}
for key, value in line_item_params.items():
self.assertEqual(item0.get(key), value)
class TestTestClasses(ZulipTestCase): class TestTestClasses(ZulipTestCase):
def test_subscribe_realm_to_manual_license_management_plan(self) -> None: def test_subscribe_realm_to_manual_license_management_plan(self) -> None:

View File

@ -472,6 +472,10 @@ def do_change_user_role(
}, },
) )
maybe_enqueue_audit_log_upload(user_profile.realm) maybe_enqueue_audit_log_upload(user_profile.realm)
if settings.BILLING_ENABLED and UserProfile.ROLE_GUEST in [old_value, value]:
billing_session = RealmBillingSession(user=user_profile, realm=user_profile.realm)
billing_session.update_license_ledger_if_needed(timezone_now())
event = dict( event = dict(
type="realm_user", op="update", person=dict(user_id=user_profile.id, role=user_profile.role) type="realm_user", op="update", person=dict(user_id=user_profile.id, role=user_profile.role)
) )