diff --git a/corporate/lib/activity.py b/corporate/lib/activity.py index ef849eccda..5f8e0f78c4 100644 --- a/corporate/lib/activity.py +++ b/corporate/lib/activity.py @@ -16,11 +16,6 @@ from django.utils.timezone import now as timezone_now from markupsafe import Markup from psycopg2.sql import Composable -from corporate.lib.stripe import ( - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, -) from corporate.models import CustomerPlan, LicenseLedger from zerver.lib.pysa import mark_sanitized from zerver.lib.url_encoding import append_url_query_string @@ -196,6 +191,8 @@ def get_remote_activity_plan_data( remote_realm: RemoteRealm | None = None, remote_server: RemoteZulipServer | None = None, ) -> RemoteActivityPlanData: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + if plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY or plan.status in ( CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, @@ -226,6 +223,8 @@ def get_remote_activity_plan_data( def get_estimated_arr_and_rate_by_realm() -> tuple[dict[str, int], dict[str, str]]: # nocoverage + from corporate.lib.stripe import RealmBillingSession + # NOTE: Customers without a plan might still have a discount attached to them which # are not included in `plan_rate`. annual_revenue = {} diff --git a/corporate/lib/decorator.py b/corporate/lib/decorator.py index bd1b57dc59..459fa0897b 100644 --- a/corporate/lib/decorator.py +++ b/corporate/lib/decorator.py @@ -1,7 +1,7 @@ import inspect from collections.abc import Callable from functools import wraps -from typing import Concatenate +from typing import TYPE_CHECKING, Concatenate from urllib.parse import urlencode, urljoin from django.conf import settings @@ -15,12 +15,14 @@ from corporate.lib.remote_billing_util import ( get_remote_realm_and_user_from_session, get_remote_server_and_user_from_session, ) -from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession from zerver.lib.exceptions import RemoteBillingAuthenticationError from zerver.lib.subdomains import get_subdomain from zerver.lib.url_encoding import append_url_query_string from zilencer.models import RemoteRealm +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + ParamT = ParamSpec("ParamT") @@ -54,7 +56,9 @@ def self_hosting_management_endpoint( def authenticated_remote_realm_management_endpoint( - view_func: Callable[Concatenate[HttpRequest, RemoteRealmBillingSession, ParamT], HttpResponse], + view_func: Callable[ + Concatenate[HttpRequest, "RemoteRealmBillingSession", ParamT], HttpResponse + ], ) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]: @wraps(view_func) def _wrapped_view_func( @@ -63,6 +67,8 @@ def authenticated_remote_realm_management_endpoint( *args: ParamT.args, **kwargs: ParamT.kwargs, ) -> HttpResponse: + from corporate.lib.stripe import RemoteRealmBillingSession + if not is_self_hosting_management_subdomain(request): # nocoverage return render(request, "404.html", status=404) @@ -160,7 +166,9 @@ def get_next_page_param_from_request_path(request: HttpRequest) -> str | None: def authenticated_remote_server_management_endpoint( - view_func: Callable[Concatenate[HttpRequest, RemoteServerBillingSession, ParamT], HttpResponse], + view_func: Callable[ + Concatenate[HttpRequest, "RemoteServerBillingSession", ParamT], HttpResponse + ], ) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]: @wraps(view_func) def _wrapped_view_func( @@ -169,6 +177,8 @@ def authenticated_remote_server_management_endpoint( *args: ParamT.args, **kwargs: ParamT.kwargs, ) -> HttpResponse: + from corporate.lib.stripe import RemoteServerBillingSession + if not is_self_hosting_management_subdomain(request): # nocoverage return render(request, "404.html", status=404) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index c578036251..5fe87bebf9 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -4976,7 +4976,9 @@ class StripeWebhookEndpointTest(ZulipTestCase): "type": "checkout.session.completed", "data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}}, } - with patch("corporate.views.webhook.handle_checkout_session_completed_event") as m: + with patch( + "corporate.lib.stripe_event_handler.handle_checkout_session_completed_event" + ) as m: result = self.client_post( "/stripe/webhook/", valid_session_event_data, @@ -4998,7 +5000,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): "data": {"object": {"object": "invoice", "id": stripe_invoice_id}}, } - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_session_event_data, @@ -5015,7 +5017,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): ) self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 0) - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_session_event_data, @@ -5026,7 +5028,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): strip_event = stripe.Event.construct_from(valid_session_event_data, stripe.api_key) m.assert_called_once_with(strip_event.data.object, event) - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_session_event_data, @@ -5048,7 +5050,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): "data": {"object": {"object": "invoice", "id": stripe_invoice_id}}, } - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_invoice_paid_event_data, @@ -5065,7 +5067,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): ) self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 0) - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_invoice_paid_event_data, @@ -5076,7 +5078,7 @@ class StripeWebhookEndpointTest(ZulipTestCase): strip_event = stripe.Event.construct_from(valid_invoice_paid_event_data, stripe.api_key) m.assert_called_once_with(strip_event.data.object, event) - with patch("corporate.views.webhook.handle_invoice_paid_event") as m: + with patch("corporate.lib.stripe_event_handler.handle_invoice_paid_event") as m: result = self.client_post( "/stripe/webhook/", valid_invoice_paid_event_data, diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 1c4a5abd00..b29ff66603 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -1,5 +1,5 @@ import logging -from typing import Annotated, Any, Literal +from typing import TYPE_CHECKING, Annotated, Any, Literal from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect from django.shortcuts import render @@ -11,14 +11,6 @@ from corporate.lib.decorator import ( authenticated_remote_realm_management_endpoint, authenticated_remote_server_management_endpoint, ) -from corporate.lib.stripe import ( - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, - ServerDeactivateWithExistingPlanError, - UpdatePlanRequest, - do_deactivate_remote_server, -) from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm from zerver.decorator import process_as_post, require_billing_access, zulip_login_required from zerver.lib.exceptions import JsonableError @@ -29,6 +21,10 @@ from zerver.models import UserProfile from zilencer.lib.remote_counts import MissingDataError from zilencer.models import RemoteRealm, RemoteZulipServer +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + + billing_logger = logging.getLogger("corporate.stripe") ALLOWED_PLANS_API_STATUS_VALUES = [ @@ -49,6 +45,8 @@ def billing_page( *, success_message: str = "", ) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession + user = request.user assert user.is_authenticated @@ -91,11 +89,11 @@ def billing_page( return render(request, "corporate/billing/billing.html", context=context) -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def remote_realm_billing_page( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, success_message: str = "", ) -> HttpResponse: @@ -152,11 +150,11 @@ def remote_realm_billing_page( return render(request, "corporate/billing/billing.html", context=context) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_billing_page( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, success_message: str = "", ) -> HttpResponse: @@ -246,6 +244,8 @@ def update_plan( licenses_at_next_renewal: Json[int] | None = None, schedule: Json[int] | None = None, ) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest + update_plan_request = UpdatePlanRequest( status=status, licenses=licenses, @@ -257,12 +257,12 @@ def update_plan( return json_success(request) -@authenticated_remote_realm_management_endpoint @process_as_post @typed_endpoint +@authenticated_remote_realm_management_endpoint def update_plan_for_remote_realm( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, status: Annotated[ Json[int], AfterValidator(lambda x: check_int_in(x, ALLOWED_PLANS_API_STATUS_VALUES)) @@ -272,6 +272,8 @@ def update_plan_for_remote_realm( licenses_at_next_renewal: Json[int] | None = None, schedule: Json[int] | None = None, ) -> HttpResponse: + from corporate.lib.stripe import UpdatePlanRequest + update_plan_request = UpdatePlanRequest( status=status, licenses=licenses, @@ -282,12 +284,12 @@ def update_plan_for_remote_realm( return json_success(request) -@authenticated_remote_server_management_endpoint @process_as_post @typed_endpoint +@authenticated_remote_server_management_endpoint def update_plan_for_remote_server( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, status: Annotated[ Json[int], AfterValidator(lambda x: check_int_in(x, ALLOWED_PLANS_API_STATUS_VALUES)) @@ -297,6 +299,8 @@ def update_plan_for_remote_server( licenses_at_next_renewal: Json[int] | None = None, schedule: Json[int] | None = None, ) -> HttpResponse: + from corporate.lib.stripe import UpdatePlanRequest + update_plan_request = UpdatePlanRequest( status=status, licenses=licenses, @@ -307,14 +311,19 @@ def update_plan_for_remote_server( return json_success(request) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_deactivate_page( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, confirmed: Literal[None, "true"] = None, ) -> HttpResponse: + from corporate.lib.stripe import ( + ServerDeactivateWithExistingPlanError, + do_deactivate_remote_server, + ) + if request.method not in ["GET", "POST"]: # nocoverage return HttpResponseNotAllowed(["GET", "POST"]) diff --git a/corporate/views/event_status.py b/corporate/views/event_status.py index 64733a6b79..eca8c346d7 100644 --- a/corporate/views/event_status.py +++ b/corporate/views/event_status.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING from django.http import HttpRequest, HttpResponse from django.shortcuts import render @@ -8,17 +9,14 @@ from corporate.lib.decorator import ( authenticated_remote_server_management_endpoint, self_hosting_management_endpoint, ) -from corporate.lib.stripe import ( - EventStatusRequest, - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, -) from zerver.decorator import require_organization_member, zulip_login_required from zerver.lib.response import json_success from zerver.lib.typed_endpoint import typed_endpoint from zerver.models import UserProfile +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + billing_logger = logging.getLogger("corporate.stripe") @@ -31,6 +29,8 @@ def event_status( stripe_session_id: str | None = None, stripe_invoice_id: str | None = None, ) -> HttpResponse: + from corporate.lib.stripe import EventStatusRequest, RealmBillingSession + event_status_request = EventStatusRequest( stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id ) @@ -39,15 +39,17 @@ def event_status( return json_success(request, data) -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def remote_realm_event_status( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, stripe_session_id: str | None = None, stripe_invoice_id: str | None = None, ) -> HttpResponse: + from corporate.lib.stripe import EventStatusRequest + event_status_request = EventStatusRequest( stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id ) @@ -55,15 +57,17 @@ def remote_realm_event_status( return json_success(request, data) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_event_status( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, stripe_session_id: str | None = None, stripe_invoice_id: str | None = None, ) -> HttpResponse: # nocoverage + from corporate.lib.stripe import EventStatusRequest + event_status_request = EventStatusRequest( stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id ) diff --git a/corporate/views/installation_activity.py b/corporate/views/installation_activity.py index 5c487ac5b7..53e919cbc1 100644 --- a/corporate/views/installation_activity.py +++ b/corporate/views/installation_activity.py @@ -25,7 +25,6 @@ from corporate.lib.activity import ( realm_support_link, realm_url_link, ) -from corporate.lib.stripe import cents_to_dollar_string from corporate.views.support import get_plan_type_string from zerver.decorator import require_server_admin from zerver.lib.typed_endpoint import typed_endpoint @@ -92,6 +91,8 @@ def get_realm_day_counts() -> dict[str, dict[str, Markup]]: def realm_summary_table(export: bool) -> str: + from corporate.lib.stripe import cents_to_dollar_string + now = timezone_now() query = SQL( diff --git a/corporate/views/portico.py b/corporate/views/portico.py index dd4de6644d..201c431117 100644 --- a/corporate/views/portico.py +++ b/corporate/views/portico.py @@ -1,4 +1,5 @@ from dataclasses import asdict, dataclass +from typing import TYPE_CHECKING from urllib.parse import urlencode import orjson @@ -13,13 +14,6 @@ from corporate.lib.decorator import ( authenticated_remote_realm_management_endpoint, authenticated_remote_server_management_endpoint, ) -from corporate.lib.stripe import ( - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, - get_configured_fixed_price_plan_offer, - get_free_trial_days, -) from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm from zerver.context_processors import get_realm_from_request, latest_info_context from zerver.decorator import add_google_analytics, zulip_login_required @@ -33,6 +27,9 @@ from zerver.lib.subdomains import is_subdomain_root_or_alias from zerver.lib.typed_endpoint import typed_endpoint from zerver.models import Realm +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + @add_google_analytics def apps_view(request: HttpRequest, platform: str | None = None) -> HttpResponse: @@ -95,6 +92,8 @@ class PlansPageContext: @add_google_analytics def plans_view(request: HttpRequest) -> HttpResponse: + from corporate.lib.stripe import get_free_trial_days + realm = get_realm_from_request(request) context = PlansPageContext( is_cloud_realm=True, @@ -138,8 +137,10 @@ def plans_view(request: HttpRequest) -> HttpResponse: @add_google_analytics @authenticated_remote_realm_management_endpoint def remote_realm_plans_page( - request: HttpRequest, billing_session: RemoteRealmBillingSession + request: HttpRequest, billing_session: "RemoteRealmBillingSession" ) -> HttpResponse: + from corporate.lib.stripe import get_configured_fixed_price_plan_offer, get_free_trial_days + customer = billing_session.get_customer() context = PlansPageContext( is_self_hosted_realm=True, @@ -205,8 +206,10 @@ def remote_realm_plans_page( @add_google_analytics @authenticated_remote_server_management_endpoint def remote_server_plans_page( - request: HttpRequest, billing_session: RemoteServerBillingSession + request: HttpRequest, billing_session: "RemoteServerBillingSession" ) -> HttpResponse: + from corporate.lib.stripe import get_configured_fixed_price_plan_offer, get_free_trial_days + customer = billing_session.get_customer() context = PlansPageContext( is_self_hosted_realm=True, @@ -377,6 +380,8 @@ def communities_view(request: HttpRequest) -> HttpResponse: @zulip_login_required def invoices_page(request: HttpRequest) -> HttpResponseRedirect: + from corporate.lib.stripe import RealmBillingSession + user = request.user assert user.is_authenticated @@ -390,7 +395,7 @@ def invoices_page(request: HttpRequest) -> HttpResponseRedirect: @authenticated_remote_realm_management_endpoint def remote_realm_invoices_page( - request: HttpRequest, billing_session: RemoteRealmBillingSession + request: HttpRequest, billing_session: "RemoteRealmBillingSession" ) -> HttpResponseRedirect: list_invoices_session_url = billing_session.get_past_invoices_session_url() return HttpResponseRedirect(list_invoices_session_url) @@ -398,7 +403,7 @@ def remote_realm_invoices_page( @authenticated_remote_server_management_endpoint def remote_server_invoices_page( - request: HttpRequest, billing_session: RemoteServerBillingSession + request: HttpRequest, billing_session: "RemoteServerBillingSession" ) -> HttpResponseRedirect: list_invoices_session_url = billing_session.get_past_invoices_session_url() return HttpResponseRedirect(list_invoices_session_url) @@ -414,6 +419,8 @@ def customer_portal( tier: Json[int] | None = None, setup_payment_by_invoice: Json[bool] = False, ) -> HttpResponseRedirect: + from corporate.lib.stripe import RealmBillingSession + user = request.user assert user.is_authenticated @@ -427,11 +434,11 @@ def customer_portal( return HttpResponseRedirect(review_billing_information_url) -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def remote_realm_customer_portal( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, return_to_billing_page: Json[bool] = False, manual_license_management: Json[bool] = False, @@ -444,11 +451,11 @@ def remote_realm_customer_portal( return HttpResponseRedirect(review_billing_information_url) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_customer_portal( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, return_to_billing_page: Json[bool] = False, manual_license_management: Json[bool] = False, diff --git a/corporate/views/remote_activity.py b/corporate/views/remote_activity.py index a5f7ece480..f7e244ebda 100644 --- a/corporate/views/remote_activity.py +++ b/corporate/views/remote_activity.py @@ -16,7 +16,6 @@ from corporate.lib.activity import ( remote_installation_stats_link, remote_installation_support_link, ) -from corporate.lib.stripe import cents_to_dollar_string from zerver.decorator import require_server_admin from zerver.models.realms import get_org_type_display_name from zilencer.models import get_remote_customer_user_count @@ -24,6 +23,8 @@ from zilencer.models import get_remote_customer_user_count @require_server_admin def get_remote_server_activity(request: HttpRequest) -> HttpResponse: + from corporate.lib.stripe import cents_to_dollar_string + title = "Remote servers" query = SQL( diff --git a/corporate/views/remote_billing_page.py b/corporate/views/remote_billing_page.py index 77a9eec346..9021beee46 100644 --- a/corporate/views/remote_billing_page.py +++ b/corporate/views/remote_billing_page.py @@ -32,11 +32,6 @@ from corporate.lib.remote_billing_util import ( RemoteBillingUserDict, get_remote_server_and_user_from_session, ) -from corporate.lib.stripe import ( - BILLING_SUPPORT_EMAIL, - RemoteRealmBillingSession, - RemoteServerBillingSession, -) from corporate.models import ( CustomerPlan, get_current_plan_by_customer, @@ -168,6 +163,8 @@ def remote_realm_billing_finalize_login( This is the endpoint accessed via the billing_access_url, generated by remote_realm_billing_entry entry. """ + from corporate.lib.stripe import RemoteRealmBillingSession + if request.method not in ["GET", "POST"]: return HttpResponseNotAllowed(["GET", "POST"]) tos_consent_given = tos_consent == "true" @@ -375,6 +372,8 @@ def remote_realm_billing_confirm_email( a fully authenticated session. """ + from corporate.lib.stripe import BILLING_SUPPORT_EMAIL + identity_dict = get_identity_dict_from_signed_access_token(signed_billing_access_token) try: remote_server = get_remote_server_by_uuid(identity_dict["remote_server_uuid"]) @@ -600,6 +599,8 @@ def remote_billing_legacy_server_confirm_login( a fully authenticated session. """ + from corporate.lib.stripe import BILLING_SUPPORT_EMAIL + try: remote_server, remote_billing_user = get_remote_server_and_user_from_session( request, server_uuid=server_uuid @@ -681,6 +682,8 @@ def remote_billing_legacy_server_from_login_confirmation_link( """ The user comes here via the confirmation link they received via email. """ + from corporate.lib.stripe import RemoteServerBillingSession + if request.method not in ["GET", "POST"]: return HttpResponseNotAllowed(["GET", "POST"]) diff --git a/corporate/views/session.py b/corporate/views/session.py index ec98e5caa1..75a4e5adb4 100644 --- a/corporate/views/session.py +++ b/corporate/views/session.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING from django.http import HttpRequest, HttpResponse from pydantic import Json @@ -7,21 +8,21 @@ from corporate.lib.decorator import ( authenticated_remote_realm_management_endpoint, authenticated_remote_server_management_endpoint, ) -from corporate.lib.stripe import ( - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, -) from zerver.decorator import require_billing_access, require_organization_member from zerver.lib.response import json_success from zerver.lib.typed_endpoint import typed_endpoint from zerver.models import UserProfile +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + billing_logger = logging.getLogger("corporate.stripe") @require_billing_access def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession + billing_session = RealmBillingSession(user) session_data = billing_session.create_card_update_session() return json_success( @@ -32,7 +33,7 @@ def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> @authenticated_remote_realm_management_endpoint def start_card_update_stripe_session_for_remote_realm( - request: HttpRequest, billing_session: RemoteRealmBillingSession + request: HttpRequest, billing_session: "RemoteRealmBillingSession" ) -> HttpResponse: # nocoverage session_data = billing_session.create_card_update_session() return json_success( @@ -43,7 +44,7 @@ def start_card_update_stripe_session_for_remote_realm( @authenticated_remote_server_management_endpoint def start_card_update_stripe_session_for_remote_server( - request: HttpRequest, billing_session: RemoteServerBillingSession + request: HttpRequest, billing_session: "RemoteServerBillingSession" ) -> HttpResponse: # nocoverage session_data = billing_session.create_card_update_session() return json_success( @@ -61,6 +62,8 @@ def start_card_update_stripe_session_for_realm_upgrade( manual_license_management: Json[bool] = False, tier: Json[int], ) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession + billing_session = RealmBillingSession(user) session_data = billing_session.create_card_update_session_for_upgrade( manual_license_management, tier @@ -71,11 +74,11 @@ def start_card_update_stripe_session_for_realm_upgrade( ) -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def start_card_update_stripe_session_for_remote_realm_upgrade( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, manual_license_management: Json[bool] = False, tier: Json[int], @@ -89,11 +92,11 @@ def start_card_update_stripe_session_for_remote_realm_upgrade( ) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def start_card_update_stripe_session_for_remote_server_upgrade( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, manual_license_management: Json[bool] = False, tier: Json[int], diff --git a/corporate/views/sponsorship.py b/corporate/views/sponsorship.py index 88439bac1e..f0d4a2fb79 100644 --- a/corporate/views/sponsorship.py +++ b/corporate/views/sponsorship.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -6,19 +8,18 @@ from corporate.lib.decorator import ( authenticated_remote_realm_management_endpoint, authenticated_remote_server_management_endpoint, ) -from corporate.lib.stripe import ( - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, - SponsorshipRequestForm, -) from zerver.decorator import require_organization_member, zulip_login_required from zerver.lib.response import json_success from zerver.models import UserProfile +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + @zulip_login_required def sponsorship_page(request: HttpRequest) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession + user = request.user assert user.is_authenticated @@ -33,7 +34,7 @@ def sponsorship_page(request: HttpRequest) -> HttpResponse: @authenticated_remote_realm_management_endpoint def remote_realm_sponsorship_page( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", ) -> HttpResponse: # nocoverage context = billing_session.get_sponsorship_request_context() if context is None: @@ -47,7 +48,7 @@ def remote_realm_sponsorship_page( @authenticated_remote_server_management_endpoint def remote_server_sponsorship_page( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", ) -> HttpResponse: # nocoverage context = billing_session.get_sponsorship_request_context() if context is None: @@ -63,6 +64,8 @@ def sponsorship( request: HttpRequest, user: UserProfile, ) -> HttpResponse: + from corporate.lib.stripe import RealmBillingSession, SponsorshipRequestForm + billing_session = RealmBillingSession(user) post_data = request.POST.copy() form = SponsorshipRequestForm(post_data) @@ -73,8 +76,10 @@ def sponsorship( @authenticated_remote_realm_management_endpoint def remote_realm_sponsorship( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", ) -> HttpResponse: # nocoverage + from corporate.lib.stripe import SponsorshipRequestForm + post_data = request.POST.copy() form = SponsorshipRequestForm(post_data) billing_session.request_sponsorship(form) @@ -84,8 +89,10 @@ def remote_realm_sponsorship( @authenticated_remote_server_management_endpoint def remote_server_sponsorship( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", ) -> HttpResponse: # nocoverage + from corporate.lib.stripe import SponsorshipRequestForm + post_data = request.POST.copy() form = SponsorshipRequestForm(post_data) billing_session.request_sponsorship(form) diff --git a/corporate/views/support.py b/corporate/views/support.py index e63f8b57bf..bcac54854a 100644 --- a/corporate/views/support.py +++ b/corporate/views/support.py @@ -24,26 +24,6 @@ from confirmation.models import Confirmation, confirmation_url from confirmation.settings import STATUS_USED from corporate.lib.activity import format_optional_datetime, remote_installation_stats_link from corporate.lib.billing_types import BillingModality -from corporate.lib.stripe import ( - BILLING_SUPPORT_EMAIL, - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, - ServerDeactivateWithExistingPlanError, - SupportRequestError, - SupportType, - SupportViewRequest, - cents_to_dollar_string, - do_deactivate_remote_server, - do_reactivate_remote_server, -) -from corporate.lib.support import ( - CloudSupportData, - RemoteSupportData, - get_data_for_cloud_support_view, - get_data_for_remote_support_view, - get_realm_support_url, -) from corporate.models import CustomerPlan from zerver.actions.create_realm import do_change_realm_subdomain from zerver.actions.realm_settings import ( @@ -118,6 +98,8 @@ class DemoRequestForm(forms.Form): @zulip_login_required @typed_endpoint_without_parameters def support_request(request: HttpRequest) -> HttpResponse: + from corporate.lib.support import get_realm_support_url + user = request.user assert user.is_authenticated @@ -161,6 +143,8 @@ def support_request(request: HttpRequest) -> HttpResponse: @typed_endpoint_without_parameters def demo_request(request: HttpRequest) -> HttpResponse: + from corporate.lib.stripe import BILLING_SUPPORT_EMAIL + context = { "MAX_INPUT_LENGTH": DemoRequestForm.MAX_INPUT_LENGTH, "SORTED_ORG_TYPE_NAMES": DemoRequestForm.SORTED_ORG_TYPE_NAMES, @@ -340,11 +324,15 @@ ModifyPlan = Literal[ RemoteServerStatus = Literal["active", "deactivated"] -SHARED_SUPPORT_CONTEXT = { - "get_org_type_display_name": get_org_type_display_name, - "get_plan_type_name": get_plan_type_string, - "dollar_amount": cents_to_dollar_string, -} + +def shared_support_context() -> dict[str, object]: + from corporate.lib.stripe import cents_to_dollar_string + + return { + "get_org_type_display_name": get_org_type_display_name, + "get_plan_type_name": get_plan_type_string, + "dollar_amount": cents_to_dollar_string, + } @require_server_admin @@ -370,7 +358,15 @@ def support( org_type: Json[NonNegativeInt] | None = None, max_invites: Json[NonNegativeInt] | None = None, ) -> HttpResponse: - context: dict[str, Any] = {**SHARED_SUPPORT_CONTEXT} + from corporate.lib.stripe import ( + RealmBillingSession, + SupportRequestError, + SupportType, + SupportViewRequest, + ) + from corporate.lib.support import CloudSupportData, get_data_for_cloud_support_view + + context = shared_support_context() if "success_message" in request.session: context["success_message"] = request.session["success_message"] @@ -692,7 +688,19 @@ def remote_servers_support( ] | None = None, ) -> HttpResponse: - context: dict[str, Any] = {**SHARED_SUPPORT_CONTEXT} + from corporate.lib.stripe import ( + RemoteRealmBillingSession, + RemoteServerBillingSession, + ServerDeactivateWithExistingPlanError, + SupportRequestError, + SupportType, + SupportViewRequest, + do_deactivate_remote_server, + do_reactivate_remote_server, + ) + from corporate.lib.support import RemoteSupportData, get_data_for_remote_support_view + + context = shared_support_context() if "success_message" in request.session: context["success_message"] = request.session["success_message"] diff --git a/corporate/views/upgrade.py b/corporate/views/upgrade.py index ea4382972c..a49f82af65 100644 --- a/corporate/views/upgrade.py +++ b/corporate/views/upgrade.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING from django.conf import settings from django.http import HttpRequest, HttpResponse, HttpResponseRedirect @@ -10,14 +11,6 @@ from corporate.lib.decorator import ( authenticated_remote_realm_management_endpoint, authenticated_remote_server_management_endpoint, ) -from corporate.lib.stripe import ( - BillingError, - InitialUpgradeRequest, - RealmBillingSession, - RemoteRealmBillingSession, - RemoteServerBillingSession, - UpgradeRequest, -) from corporate.models import CustomerPlan from zerver.decorator import require_organization_member, zulip_login_required from zerver.lib.response import json_success @@ -25,6 +18,9 @@ from zerver.lib.typed_endpoint import typed_endpoint from zerver.models import UserProfile from zilencer.lib.remote_counts import MissingDataError +if TYPE_CHECKING: + from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession + billing_logger = logging.getLogger("corporate.stripe") @@ -42,6 +38,8 @@ def upgrade( licenses: Json[int] | None = None, tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD, ) -> HttpResponse: + from corporate.lib.stripe import BillingError, RealmBillingSession, UpgradeRequest + try: upgrade_request = UpgradeRequest( billing_modality=billing_modality, @@ -77,11 +75,11 @@ def upgrade( raise BillingError(error_description, error_message) -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def remote_realm_upgrade( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, billing_modality: BillingModality, schedule: BillingSchedule, @@ -92,6 +90,8 @@ def remote_realm_upgrade( remote_server_plan_start_date: str | None = None, tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS, ) -> HttpResponse: + from corporate.lib.stripe import BillingError, UpgradeRequest + try: upgrade_request = UpgradeRequest( billing_modality=billing_modality, @@ -125,11 +125,11 @@ def remote_realm_upgrade( raise BillingError(error_description, error_message) -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_upgrade( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, billing_modality: BillingModality, schedule: BillingSchedule, @@ -140,6 +140,8 @@ def remote_server_upgrade( remote_server_plan_start_date: str | None = None, tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS, ) -> HttpResponse: + from corporate.lib.stripe import BillingError, UpgradeRequest + try: upgrade_request = UpgradeRequest( billing_modality=billing_modality, @@ -182,6 +184,8 @@ def upgrade_page( tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD, setup_payment_by_invoice: Json[bool] = False, ) -> HttpResponse: + from corporate.lib.stripe import InitialUpgradeRequest, RealmBillingSession + user = request.user assert user.is_authenticated @@ -207,17 +211,19 @@ def upgrade_page( return response -@authenticated_remote_realm_management_endpoint @typed_endpoint +@authenticated_remote_realm_management_endpoint def remote_realm_upgrade_page( request: HttpRequest, - billing_session: RemoteRealmBillingSession, + billing_session: "RemoteRealmBillingSession", *, manual_license_management: Json[bool] = False, success_message: str = "", tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS), setup_payment_by_invoice: Json[bool] = False, ) -> HttpResponse: + from corporate.lib.stripe import InitialUpgradeRequest + billing_modality = "charge_automatically" if setup_payment_by_invoice: # nocoverage billing_modality = "send_invoice" @@ -240,17 +246,19 @@ def remote_realm_upgrade_page( return response -@authenticated_remote_server_management_endpoint @typed_endpoint +@authenticated_remote_server_management_endpoint def remote_server_upgrade_page( request: HttpRequest, - billing_session: RemoteServerBillingSession, + billing_session: "RemoteServerBillingSession", *, manual_license_management: Json[bool] = False, success_message: str = "", tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS), setup_payment_by_invoice: Json[bool] = False, ) -> HttpResponse: + from corporate.lib.stripe import InitialUpgradeRequest + billing_modality = "charge_automatically" if setup_payment_by_invoice: # nocoverage billing_modality = "send_invoice" diff --git a/corporate/views/webhook.py b/corporate/views/webhook.py index 28b6dd1b46..dcd6c9c8bf 100644 --- a/corporate/views/webhook.py +++ b/corporate/views/webhook.py @@ -1,17 +1,11 @@ import json import logging -import stripe from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.http import HttpRequest, HttpResponse from django.views.decorators.csrf import csrf_exempt -from corporate.lib.stripe import STRIPE_API_VERSION -from corporate.lib.stripe_event_handler import ( - handle_checkout_session_completed_event, - handle_invoice_paid_event, -) from corporate.models import Event, Invoice, Session from zproject.config import get_secret @@ -20,6 +14,14 @@ billing_logger = logging.getLogger("corporate.stripe") @csrf_exempt def stripe_webhook(request: HttpRequest) -> HttpResponse: + import stripe + + from corporate.lib.stripe import STRIPE_API_VERSION + from corporate.lib.stripe_event_handler import ( + handle_checkout_session_completed_event, + handle_invoice_paid_event, + ) + stripe_webhook_endpoint_secret = get_secret("stripe_webhook_endpoint_secret", "") if ( stripe_webhook_endpoint_secret and not settings.TEST_SUITE diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index bffa7458a2..78805f1b85 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -48,9 +48,6 @@ from zerver.models.realms import ( from zerver.models.users import get_system_bot from zproject.backends import all_default_backend_names -if settings.CORPORATE_ENABLED: - from corporate.lib.support import get_realm_support_url - def do_change_realm_subdomain( realm: Realm, @@ -372,6 +369,8 @@ def do_create_realm( # Send a notification to the admin realm when a new organization registers. if settings.CORPORATE_ENABLED: + from corporate.lib.support import get_realm_support_url + admin_realm = get_realm(settings.SYSTEM_BOT_REALM) sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id) diff --git a/zerver/actions/create_user.py b/zerver/actions/create_user.py index acb93a4d2e..2801cdfd30 100644 --- a/zerver/actions/create_user.py +++ b/zerver/actions/create_user.py @@ -62,10 +62,6 @@ from zerver.models.realm_audit_logs import AuditLogEventType from zerver.models.users import active_user_ids, bot_owner_user_ids, get_system_bot from zerver.tornado.django_api import send_event_on_commit -if settings.BILLING_ENABLED: - from corporate.lib.stripe import RealmBillingSession - - MAX_NUM_RECENT_MESSAGES = 1000 MAX_NUM_RECENT_UNREAD_MESSAGES = 20 @@ -512,6 +508,9 @@ def do_create_user( email_address_visibility: int | None = None, add_initial_stream_subscriptions: bool = True, ) -> UserProfile: + if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + with transaction.atomic(): user_profile = create_user( email=email, @@ -643,6 +642,9 @@ def do_activate_mirror_dummy_user( parallel code path to do_create_user; e.g. it likely does not handle preferences or default streams properly. """ + if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + with transaction.atomic(): change_user_is_active(user_profile, True) user_profile.is_mirror_dummy = False @@ -714,6 +716,8 @@ def do_reactivate_user(user_profile: UserProfile, *, acting_user: UserProfile | bot_owner_changed = True if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + billing_session = RealmBillingSession(user=user_profile, realm=user_profile.realm) billing_session.update_license_ledger_if_needed(event_time) diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index e5134ff609..12d80b72fc 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -51,9 +51,6 @@ from zerver.models.realms import get_default_max_invites_for_realm_plan_type, ge from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit -if settings.BILLING_ENABLED: - from corporate.lib.stripe import RealmBillingSession - @transaction.atomic(savepoint=False) def do_set_realm_property( @@ -516,6 +513,9 @@ def do_deactivate_realm( if realm.deactivated: return + if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + with transaction.atomic(): realm.deactivated = True realm.save(update_fields=["deactivated"]) @@ -623,6 +623,8 @@ def do_delete_all_realm_attachments(realm: Realm, *, batch_size: int = 1000) -> def do_scrub_realm(realm: Realm, *, acting_user: UserProfile | None) -> None: if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + billing_session = RealmBillingSession(user=acting_user, realm=realm) billing_session.downgrade_now_without_creating_additional_invoices() diff --git a/zerver/actions/users.py b/zerver/actions/users.py index 4962770df6..4b14231717 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -53,9 +53,6 @@ from zerver.models.users import ( ) from zerver.tornado.django_api import send_event_on_commit -if settings.BILLING_ENABLED: - from corporate.lib.stripe import RealmBillingSession - def do_delete_user(user_profile: UserProfile, *, acting_user: UserProfile | None) -> None: if user_profile.realm.is_zephyr_mirror_realm: @@ -324,6 +321,9 @@ def do_deactivate_user( if not user_profile.is_active: return + if settings.BILLING_ENABLED: + from corporate.lib.stripe import RealmBillingSession + if _cascade: # We need to deactivate bots before the target user, to ensure # that a failure partway through this function cannot result @@ -474,6 +474,8 @@ def do_change_user_role( ) maybe_enqueue_audit_log_upload(user_profile.realm) if settings.BILLING_ENABLED and UserProfile.ROLE_GUEST in [old_value, value]: + from corporate.lib.stripe import RealmBillingSession + billing_session = RealmBillingSession(user=user_profile, realm=user_profile.realm) billing_session.update_license_ledger_if_needed(timezone_now()) diff --git a/zerver/forms.py b/zerver/forms.py index 0ff3c95499..96630b9e9c 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -46,10 +46,6 @@ from zerver.models.realms import ( from zerver.models.users import get_user_by_delivery_email, is_cross_realm_bot_email from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap -if settings.BILLING_ENABLED: - from corporate.lib.registration import check_spare_licenses_available_for_registering_new_user - from corporate.lib.stripe import LicenseLimitError - # We don't mark this error for translation, because it's displayed # only to MIT users. MIT_VALIDATION_ERROR = Markup( @@ -302,6 +298,11 @@ class HomepageForm(forms.Form): email_is_not_mit_mailing_list(email) if settings.BILLING_ENABLED: + from corporate.lib.registration import ( + check_spare_licenses_available_for_registering_new_user, + ) + from corporate.lib.stripe import LicenseLimitError + role = self.invited_as if self.invited_as is not None else UserProfile.ROLE_MEMBER try: check_spare_licenses_available_for_registering_new_user(realm, email, role=role) diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index ebc545b800..7166f5adfe 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -2372,7 +2372,7 @@ class AnalyticsBouncerTest(BouncerTestCase): with ( mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=None + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", return_value=None ) as m, mock.patch( "corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses", @@ -2404,7 +2404,8 @@ class AnalyticsBouncerTest(BouncerTestCase): dummy_customer = mock.MagicMock() with ( mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", + return_value=dummy_customer, ), mock.patch("corporate.lib.stripe.get_current_plan_by_customer", return_value=None) as m, mock.patch( @@ -2425,7 +2426,8 @@ class AnalyticsBouncerTest(BouncerTestCase): with ( mock.patch( - "zilencer.views.RemoteRealmBillingSession.get_customer", return_value=dummy_customer + "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", + return_value=dummy_customer, ), mock.patch("corporate.lib.stripe.get_current_plan_by_customer", return_value=None), mock.patch( @@ -2586,7 +2588,9 @@ class AnalyticsBouncerTest(BouncerTestCase): "corporate.lib.stripe.RemoteServerBillingSession.get_customer", return_value=dummy_remote_server_customer, ), - mock.patch("zilencer.views.RemoteServerBillingSession.sync_license_ledger_if_needed"), + mock.patch( + "corporate.lib.stripe.RemoteServerBillingSession.sync_license_ledger_if_needed" + ), mock.patch( "corporate.lib.stripe.get_current_plan_by_customer", side_effect=get_current_plan_by_customer, @@ -2702,7 +2706,9 @@ class AnalyticsBouncerTest(BouncerTestCase): # of a deleted realm. with ( self.assertLogs(logger, level="WARNING") as analytics_logger, - mock.patch("zilencer.views.RemoteRealmBillingSession.on_paid_plan", return_value=True), + mock.patch( + "corporate.lib.stripe.RemoteRealmBillingSession.on_paid_plan", return_value=True + ), ): # This time the logger shouldn't get triggered - because the bouncer doesn't # include .realm_locally_deleted realms in its response. diff --git a/zerver/views/registration.py b/zerver/views/registration.py index ead5f80a01..ee59964826 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -120,10 +120,6 @@ from zproject.backends import ( password_auth_enabled, ) -if settings.BILLING_ENABLED: - from corporate.lib.registration import check_spare_licenses_available_for_registering_new_user - from corporate.lib.stripe import LicenseLimitError - @typed_endpoint def get_prereg_key_and_redirect( @@ -332,6 +328,11 @@ def registration_helper( return redirect_to_email_login_url(email) if settings.BILLING_ENABLED: + from corporate.lib.registration import ( + check_spare_licenses_available_for_registering_new_user, + ) + from corporate.lib.stripe import LicenseLimitError + try: check_spare_licenses_available_for_registering_new_user(realm, email, role=role) except LicenseLimitError: diff --git a/zilencer/views.py b/zilencer/views.py index a47723e49b..b2076f70c2 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -30,13 +30,6 @@ from analytics.lib.counts import ( REMOTE_INSTALLATION_COUNT_STATS, do_increment_logging_stat, ) -from corporate.lib.stripe import ( - BILLING_SUPPORT_EMAIL, - RemoteRealmBillingSession, - RemoteServerBillingSession, - do_deactivate_remote_server, - get_push_status_for_remote_request, -) from corporate.models import ( CustomerPlan, get_current_plan_by_customer, @@ -120,6 +113,8 @@ def deactivate_remote_server( request: HttpRequest, remote_server: RemoteZulipServer, ) -> HttpResponse: + from corporate.lib.stripe import RemoteServerBillingSession, do_deactivate_remote_server + billing_session = RemoteServerBillingSession(remote_server) do_deactivate_remote_server(remote_server, billing_session) return json_success(request) @@ -538,6 +533,8 @@ def remote_server_notify_push( *, payload: JsonBodyPayload[RemoteServerNotificationPayload], ) -> HttpResponse: + from corporate.lib.stripe import get_push_status_for_remote_request + user_id = payload.user_id user_uuid = payload.user_uuid user_identity = UserPushIdentityCompat(user_id, user_uuid) @@ -844,6 +841,8 @@ def ensure_devices_set_remote_realm( def update_remote_realm_data_for_server( server: RemoteZulipServer, server_realms_info: list[RealmDataForAnalytics] ) -> None: + from corporate.lib.stripe import BILLING_SUPPORT_EMAIL, RemoteRealmBillingSession + reported_uuids = [realm.uuid for realm in server_realms_info] all_registered_remote_realms_for_server = list(RemoteRealm.objects.filter(server=server)) already_registered_remote_realms = [ @@ -1032,6 +1031,8 @@ def get_human_user_realm_uuids( def handle_customer_migration_from_server_to_realm( server: RemoteZulipServer, ) -> None: + from corporate.lib.stripe import RemoteServerBillingSession + server_billing_session = RemoteServerBillingSession(server) server_customer = server_billing_session.get_customer() if server_customer is None: @@ -1160,6 +1161,12 @@ def remote_server_post_analytics( merge_base: Json[str] | None = None, api_feature_level: Json[int] | None = None, ) -> HttpResponse: + from corporate.lib.stripe import ( + RemoteRealmBillingSession, + RemoteServerBillingSession, + get_push_status_for_remote_request, + ) + # Lock the server, preventing this from racing with other # duplicate submissions of the data server = RemoteZulipServer.objects.select_for_update().get(id=server.id)