mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 13:33:24 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
23
zerver/migrations/0681_realm_can_manage_billing_group.py
Normal file
23
zerver/migrations/0681_realm_can_manage_billing_group.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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="")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user