settings: Add "can_manage_billing_group" realm setting.

Added "can_manage_billing_group" realm group permission setting
to control who can manage billing and plans in the organization.

Fixes #32745.
This commit is contained in:
Vector73
2025-03-08 21:36:05 +00:00
committed by Tim Abbott
parent 2a0f3c9746
commit 158fd58cde
43 changed files with 411 additions and 296 deletions

View File

@@ -206,7 +206,7 @@ def require_billing_access(
**kwargs: ParamT.kwargs,
) -> HttpResponse:
if not user_profile.has_billing_access:
raise JsonableError(_("Must be a billing administrator or an organization owner"))
raise JsonableError(_("Insufficient permission"))
return func(request, user_profile, *args, **kwargs)
return wrapper

View File

@@ -41,6 +41,7 @@ from zerver.lib.event_types import (
EventPresence,
EventReactionAdd,
EventReactionRemove,
EventRealmBilling,
EventRealmBotAdd,
EventRealmBotDelete,
EventRealmBotUpdate,
@@ -175,6 +176,7 @@ check_muted_users = make_checker(EventMutedUsers)
check_onboarding_steps = make_checker(EventOnboardingSteps)
check_reaction_add = make_checker(EventReactionAdd)
check_reaction_remove = make_checker(EventReactionRemove)
check_realm_billing = make_checker(EventRealmBilling)
check_realm_bot_delete = make_checker(EventRealmBotDelete)
check_realm_deactivated = make_checker(EventRealmDeactivated)
check_realm_domains_add = make_checker(EventRealmDomainsAdd)

View File

@@ -522,6 +522,7 @@ class GroupSettingUpdateData(GroupSettingUpdateDataCore):
can_delete_own_message_group: int | UserGroupMembersDict | None = None
can_invite_users_group: int | UserGroupMembersDict | None = None
can_manage_all_groups: int | UserGroupMembersDict | None = None
can_manage_billing_group: int | UserGroupMembersDict | None = None
can_mention_many_users_group: int | UserGroupMembersDict | None = None
can_move_messages_between_channels_group: int | UserGroupMembersDict | None = None
can_move_messages_between_topics_group: int | UserGroupMembersDict | None = None
@@ -683,6 +684,12 @@ class EventRealmUserUpdate(BaseEvent):
)
class EventRealmBilling(BaseEvent):
type: Literal["realm_billing"]
property: str
value: bool
class EventRestart(BaseEvent):
type: Literal["restart"]
zulip_version: str

View File

@@ -126,6 +126,24 @@ def always_want(msg_type: str) -> bool:
return True
def has_pending_sponsorship_request(
user_profile: UserProfile | None, user_has_billing_access: bool | None = None
) -> bool:
sponsorship_pending = False
if user_has_billing_access is None:
user_has_billing_access = user_profile is not None and user_profile.has_billing_access
if settings.CORPORATE_ENABLED and user_profile is not None and user_has_billing_access:
from corporate.models import get_customer_by_realm
customer = get_customer_by_realm(user_profile.realm)
if customer is not None:
sponsorship_pending = customer.sponsorship_pending
return sponsorship_pending
def fetch_initial_state_data(
user_profile: UserProfile | None,
*,
@@ -586,6 +604,23 @@ def fetch_initial_state_data(
# Set home view to recent conversations for spectators regardless of default.
web_home_view="recent_topics",
)
settings_user_recursive_group_ids = set()
if want("realm_billing") or want("realm_user"):
settings_user_recursive_group_ids = set(
get_recursive_membership_groups(settings_user).values_list("id", flat=True)
)
if want("realm_billing"):
state["realm_billing"] = {}
user_has_billing_access = (
realm.can_manage_billing_group_id in settings_user_recursive_group_ids
)
state["realm_billing"]["has_pending_sponsorship_request"] = has_pending_sponsorship_request(
settings_user, user_has_billing_access
)
if want("realm_user"):
state["raw_users"] = get_users_for_api(
realm,
@@ -614,10 +649,6 @@ def fetch_initial_state_data(
client_gravatar=False,
)
settings_user_recursive_group_ids = set(
get_recursive_membership_groups(settings_user).values_list("id", flat=True)
)
state["can_create_private_streams"] = (
realm.can_create_private_channel_group_id in settings_user_recursive_group_ids
)

View File

@@ -58,45 +58,6 @@ def promote_sponsoring_zulip_in_realm(realm: Realm) -> bool:
return realm.plan_type in [Realm.PLAN_TYPE_STANDARD_FREE, Realm.PLAN_TYPE_SELF_HOSTED]
def get_billing_info(user_profile: UserProfile | None) -> BillingInfo:
# See https://zulip.com/help/user-roles for clarity.
show_billing = False
show_plans = False
sponsorship_pending = False
# We want to always show the remote billing option as long as the user is authorized,
# except on zulipchat.com where it's not applicable.
show_remote_billing = (
(not settings.CORPORATE_ENABLED)
and user_profile is not None
and user_profile.has_billing_access
)
# This query runs on home page load, so we want to avoid
# hitting the database if possible. So, we only run it for the user
# types that can actually see the billing info.
if settings.CORPORATE_ENABLED and user_profile is not None and user_profile.has_billing_access:
from corporate.models import CustomerPlan, get_customer_by_realm
customer = get_customer_by_realm(user_profile.realm)
if customer is not None:
if customer.sponsorship_pending:
sponsorship_pending = True
if CustomerPlan.objects.filter(customer=customer).exists():
show_billing = True
if user_profile.realm.plan_type == Realm.PLAN_TYPE_LIMITED:
show_plans = True
return BillingInfo(
show_billing=show_billing,
show_plans=show_plans,
sponsorship_pending=sponsorship_pending,
show_remote_billing=show_remote_billing,
)
def get_user_permission_info(user_profile: UserProfile | None) -> UserPermissionInfo:
if user_profile is not None:
return UserPermissionInfo(
@@ -180,7 +141,6 @@ def build_page_params_for_home_page_load(
furthest_read_time = get_furthest_read_time(user_profile)
two_fa_enabled = settings.TWO_FACTOR_AUTHENTICATION_ENABLED and user_profile is not None
billing_info = get_billing_info(user_profile)
user_permission_info = get_user_permission_info(user_profile)
# Pass parameters to the client-side JavaScript code.
@@ -202,11 +162,7 @@ def build_page_params_for_home_page_load(
embedded_bots_enabled=settings.EMBEDDED_BOTS_ENABLED,
two_fa_enabled=two_fa_enabled,
apps_page_url=get_apps_page_url(),
show_billing=billing_info.show_billing,
show_remote_billing=billing_info.show_remote_billing,
promote_sponsoring_zulip=promote_sponsoring_zulip_in_realm(realm),
show_plans=billing_info.show_plans,
sponsorship_pending=billing_info.sponsorship_pending,
show_webathena=user_permission_info.show_webathena,
# Adding two_fa_enabled as condition saves us 3 queries when
# 2FA is not enabled.

View File

@@ -434,7 +434,7 @@ def send_email_to_admins(
)
def send_email_to_billing_admins_and_realm_owners(
def send_email_to_users_with_billing_access_and_realm_owners(
template_prefix: str,
realm: Realm,
from_name: str | None = None,
@@ -444,7 +444,9 @@ def send_email_to_billing_admins_and_realm_owners(
) -> None:
send_email(
template_prefix,
to_user_ids=[user.id for user in realm.get_human_billing_admin_and_realm_owner_users()],
to_user_ids=[
user.id for user in realm.get_human_users_with_billing_access_and_realm_owner_users()
],
from_name=from_name,
from_address=from_address,
language=language,

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.10 on 2025-02-17 16:11
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0680_rename_general_chat_to_empty_string_topic"),
]
operations = [
migrations.AddField(
model_name="realm",
name="can_manage_billing_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -0,0 +1,51 @@
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
def set_default_value_for_can_manage_billing_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
UserProfile = apps.get_model("zerver", "UserProfile")
UserGroup = apps.get_model("zerver", "UserGroup")
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
OWNERS_GROUP_NAME = "role:owners"
for realm in Realm.objects.all():
if realm.can_manage_billing_group is not None:
continue
owners_system_group = NamedUserGroup.objects.get(
name=OWNERS_GROUP_NAME, realm=realm, is_system_group=True
)
billing_admins = UserProfile.objects.filter(
realm=realm, is_billing_admin=True, is_bot=False, is_active=True
)
if billing_admins.exists():
user_group = UserGroup.objects.create(realm=realm)
user_group.direct_members.set(list(billing_admins))
user_group.direct_subgroups.set([owners_system_group])
realm.can_manage_billing_group = user_group
else:
realm.can_manage_billing_group = owners_system_group
realm.save(update_fields=["can_manage_billing_group"])
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0681_realm_can_manage_billing_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_can_manage_billing_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.10 on 2025-02-17 16:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0682_set_default_value_for_can_manage_billing_group"),
]
operations = [
migrations.AlterField(
model_name="realm",
name="can_manage_billing_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -367,6 +367,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# UserGroup which is allowed to manage plans and billing.
can_manage_billing_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# UserGroup which is allowed to use wildcard mentions in large channels.
can_mention_many_users_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+"
@@ -804,6 +809,13 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
allow_everyone_group=False,
default_group_name=SystemGroups.OWNERS,
),
can_manage_billing_group=GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
allow_nobody_group=True,
allow_everyone_group=False,
default_group_name=SystemGroups.ADMINISTRATORS,
),
can_mention_many_users_group=GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
@@ -955,12 +967,17 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
role__in=roles,
)
def get_human_billing_admin_and_realm_owner_users(self) -> QuerySet["UserProfile"]:
def get_human_users_with_billing_access_and_realm_owner_users(self) -> QuerySet["UserProfile"]:
from zerver.lib.user_groups import get_recursive_group_members
can_manage_billing_group_members = get_recursive_group_members(
self.can_manage_billing_group.id
)
return UserProfile.objects.filter(
Q(role=UserProfile.ROLE_REALM_OWNER) | Q(is_billing_admin=True),
realm=self,
Q(id__in=can_manage_billing_group_members)
| Q(role=UserProfile.ROLE_REALM_OWNER, realm=self, is_active=True),
is_bot=False,
is_active=True,
)
def get_active_users(self) -> QuerySet["UserProfile"]:

View File

@@ -750,7 +750,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings):
@property
def has_billing_access(self) -> bool:
return self.is_realm_owner or self.is_billing_admin
return self.has_permission("can_manage_billing_group")
@property
def is_realm_owner(self) -> bool:

View File

@@ -4819,6 +4819,16 @@ paths:
create and edit user groups.
[calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member
can_manage_billing_group:
allOf:
- description: |
A [group-setting value](/api/group-setting-values) defining the set of
users who have permission to manage plans and billing in the organization.
**Changes**: New in Zulip 10.0 (feature level ZF-51430d). Previously, only owners
and users with `is_billing_admin` property set to `true` were allowed to
manage plans and billing.
- $ref: "#/components/schemas/GroupSettingValue"
can_summarize_topics_group:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
@@ -17128,6 +17138,16 @@ paths:
create and edit user groups.
[calc-full-member]: /api/roles-and-permissions#determining-if-a-user-is-a-full-member
realm_can_manage_billing_group:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
- description: |
A [group-setting value](/api/group-setting-values) defining the set of
users who have permission to manage plans and billing in the organization.
**Changes**: New in Zulip 10.0 (feature level ZF-51430d). Previously, only owners
and users with `is_billing_admin` property set to `true` were allowed to
manage plans and billing.
realm_can_create_public_channel_group:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
@@ -18279,6 +18299,23 @@ paths:
**Changes**: New in Zulip 5.0 (feature level 74). Previously,
this was hardcoded to 90 seconds, and clients should use that as a fallback
value when interacting with servers where this field is not present.
realm_billing:
type: object
additionalProperties: false
description: |
Present if `realm_billing` is present in `fetch_event_types`.
A dictionary containing billing information of the organization.
**Changes**: New in Zulip 10.0 (feature level ZF-51430d).
properties:
has_pending_sponsorship_request:
type: boolean
description: |
Whether there is a pending sponsorship request for the organization. Note that
this field will always be `false` if the user is not in `can_manage_billing_group`.
**Changes**: New in Zulip 10.0 (feature level ZF-51430d).
realm_moderation_request_channel_id:
type: integer
description: |

View File

@@ -595,9 +595,8 @@ class PlansPageTest(ZulipTestCase):
organization_member = "hamlet"
self.login(organization_member)
result = self.client_get("/plans/", subdomain="zulip")
self.assert_in_success_response(["Current plan"], result)
self.assert_in_success_response(["/sponsorship/"], result)
self.assert_not_in_success_response(["/accounts/go/?next=%2Fsponsorship%2F"], result)
self.assertEqual(result.status_code, 302)
self.assertEqual("/billing/", result["Location"])
# Test root domain, with login on different domain
result = self.client_get("/plans/", subdomain="")

View File

@@ -1231,6 +1231,7 @@ class FetchQueriesTest(ZulipTestCase):
presence=1,
# 2 of the 3 queries here are for fetching 'realm_user_groups' data.
realm=3,
realm_billing=1,
realm_bot=1,
realm_domains=1,
realm_embedded_bots=0,

View File

@@ -16,11 +16,8 @@ from zerver.actions.create_user import do_create_user
from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property
from zerver.actions.users import change_user_is_active
from zerver.lib.compatibility import LAST_SERVER_UPGRADE_TIME, is_outdated_server
from zerver.lib.home import (
get_billing_info,
get_furthest_read_time,
promote_sponsoring_zulip_in_realm,
)
from zerver.lib.events import has_pending_sponsorship_request
from zerver.lib.home import get_furthest_read_time, promote_sponsoring_zulip_in_realm
from zerver.lib.soft_deactivation import do_soft_deactivate_users
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import (
@@ -60,11 +57,7 @@ class HomeTest(ZulipTestCase):
"presence_history_limit_days_for_web_app",
"promote_sponsoring_zulip",
"request_language",
"show_billing",
"show_plans",
"show_remote_billing",
"show_webathena",
"sponsorship_pending",
"state_data",
"test_suite",
"translation_data",
@@ -140,6 +133,7 @@ class HomeTest(ZulipTestCase):
"realm_can_delete_own_message_group",
"realm_can_invite_users_group",
"realm_can_manage_all_groups",
"realm_can_manage_billing_group",
"realm_can_mention_many_users_group",
"realm_can_move_messages_between_channels_group",
"realm_can_move_messages_between_topics_group",
@@ -244,6 +238,7 @@ class HomeTest(ZulipTestCase):
"server_typing_stopped_wait_period_milliseconds",
"server_web_public_streams_enabled",
"settings_send_digest_emails",
"realm_billing",
"starred_messages",
"stop_words",
"subscriptions",
@@ -379,11 +374,7 @@ class HomeTest(ZulipTestCase):
"promote_sponsoring_zulip",
"realm_rendered_description",
"request_language",
"show_billing",
"show_plans",
"show_remote_billing",
"show_webathena",
"sponsorship_pending",
"state_data",
"test_suite",
"translation_data",
@@ -586,7 +577,7 @@ class HomeTest(ZulipTestCase):
# Verify number of queries for Realm admin isn't much higher than for normal users.
self.login("iago")
with (
self.assert_database_query_count(52),
self.assert_database_query_count(53),
patch("zerver.lib.cache.cache_set") as cache_mock,
):
result = self._get_home_page()
@@ -1004,19 +995,14 @@ class HomeTest(ZulipTestCase):
self.assertEqual(page_params["state_data"]["max_message_id"], -1)
@activate_push_notification_service()
def test_get_billing_info(self) -> None:
def test_has_pending_sponsorship_request(self) -> None:
user = self.example_user("desdemona")
user.role = UserProfile.ROLE_REALM_OWNER
user.save(update_fields=["role"])
# realm owner, but no CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans
shiva = self.example_user("shiva")
# realm owner, but no CustomerPlan and realm plan_type SELF_HOSTED -> don't show any links
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
sponsorship_pending = has_pending_sponsorship_request(user)
self.assertFalse(sponsorship_pending)
# realm owner, with inactive CustomerPlan and realm plan_type SELF_HOSTED -> show only billing link
customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
CustomerPlan.objects.create(
customer=customer,
@@ -1026,142 +1012,28 @@ class HomeTest(ZulipTestCase):
tier=CustomerPlan.TIER_CLOUD_STANDARD,
status=CustomerPlan.ENDED,
)
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# realm owner, with inactive CustomerPlan and realm plan_type LIMITED -> show billing link and plans
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_billing)
self.assertTrue(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# Always false without CORPORATE_ENABLED
with self.settings(CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
# show_remote_billing is independent of CORPORATE_ENABLED
self.assertTrue(billing_info.show_remote_billing)
# Always false without a UserProfile
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(None)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# realm admin, with CustomerPlan and realm plan_type LIMITED -> don't show any links
# Only billing admin and realm owner have access to billing.
user.role = UserProfile.ROLE_REALM_ADMINISTRATOR
user.save(update_fields=["role"])
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# Self-hosted servers show remote billing, but not for a user without
# billing access permission.
with self.settings(CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_remote_billing)
# billing admin, with CustomerPlan and realm plan_type STANDARD -> show only billing link
user.role = UserProfile.ROLE_MEMBER
user.is_billing_admin = True
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None)
user.save(update_fields=["role", "is_billing_admin"])
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# Self-hosted servers show remote billing for billing admins.
with self.settings(CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_remote_billing)
# billing admin, with CustomerPlan and realm plan_type PLUS -> show only billing link
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_PLUS, acting_user=None)
user.save(update_fields=["role", "is_billing_admin"])
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# member, with CustomerPlan and realm plan_type STANDARD -> neither billing link or plans
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None)
user.is_billing_admin = False
user.save(update_fields=["is_billing_admin"])
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# guest, with CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans
user.role = UserProfile.ROLE_GUEST
user.save(update_fields=["role"])
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_SELF_HOSTED, acting_user=None)
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# billing admin, but no CustomerPlan and realm plan_type SELF_HOSTED -> neither billing link or plans
user.role = UserProfile.ROLE_MEMBER
user.is_billing_admin = True
user.save(update_fields=["role", "is_billing_admin"])
CustomerPlan.objects.all().delete()
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# billing admin, with sponsorship pending and realm plan_type SELF_HOSTED -> show only sponsorship pending link
# realm admin, with sponsorship pending and realm plan_type SELF_HOSTED -> show sponsorship pending link
customer.sponsorship_pending = True
customer.save(update_fields=["sponsorship_pending"])
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertTrue(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
sponsorship_pending = has_pending_sponsorship_request(user)
self.assertTrue(sponsorship_pending)
# billing admin, no customer object and realm plan_type SELF_HOSTED -> no links
customer.delete()
# Always false without CORPORATE_ENABLED
with self.settings(CORPORATE_ENABLED=False):
sponsorship_pending = has_pending_sponsorship_request(user)
self.assertFalse(sponsorship_pending)
# Always false without a UserProfile
with self.settings(CORPORATE_ENABLED=True):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
sponsorship_pending = has_pending_sponsorship_request(None)
self.assertFalse(sponsorship_pending)
# If the server doesn't have the push bouncer configured,
# remote billing should be shown anyway, as the billing endpoint
# is supposed show a useful error page.
with self.settings(ZULIP_SERVICE_PUSH_NOTIFICATIONS=False, CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_remote_billing)
# realm moderator, with CustomerPlan and realm plan_type LIMITED -> don't show any links
# Only realm admin and realm owner have access to billing.
with self.settings(CORPORATE_ENABLED=True):
sponsorship_pending = has_pending_sponsorship_request(shiva)
self.assertFalse(sponsorship_pending)
def test_promote_sponsoring_zulip_in_realm(self) -> None:
realm = get_realm("zulip")

View File

@@ -153,6 +153,7 @@ def update_realm(
can_create_write_only_bots_group: Json[GroupSettingChangeRequest] | None = None,
can_invite_users_group: Json[GroupSettingChangeRequest] | None = None,
can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None,
can_manage_billing_group: Json[GroupSettingChangeRequest] | None = None,
can_mention_many_users_group: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None,
@@ -241,6 +242,7 @@ def update_realm(
or can_create_groups is not None
or can_invite_users_group is not None
or can_manage_all_groups is not None
or can_manage_billing_group is not None
) and not user_profile.is_realm_owner:
raise OrganizationOwnerRequiredError