corporate: Import corporate.lib.stripe lazily.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2024-09-24 14:27:28 -07:00 committed by Tim Abbott
parent fcafcb24d7
commit f0f048de69
22 changed files with 256 additions and 170 deletions

View File

@ -16,11 +16,6 @@ from django.utils.timezone import now as timezone_now
from markupsafe import Markup from markupsafe import Markup
from psycopg2.sql import Composable from psycopg2.sql import Composable
from corporate.lib.stripe import (
RealmBillingSession,
RemoteRealmBillingSession,
RemoteServerBillingSession,
)
from corporate.models import CustomerPlan, LicenseLedger from corporate.models import CustomerPlan, LicenseLedger
from zerver.lib.pysa import mark_sanitized from zerver.lib.pysa import mark_sanitized
from zerver.lib.url_encoding import append_url_query_string 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_realm: RemoteRealm | None = None,
remote_server: RemoteZulipServer | None = None, remote_server: RemoteZulipServer | None = None,
) -> RemoteActivityPlanData: ) -> RemoteActivityPlanData:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
if plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY or plan.status in ( if plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY or plan.status in (
CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL, CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL,
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, 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 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 # NOTE: Customers without a plan might still have a discount attached to them which
# are not included in `plan_rate`. # are not included in `plan_rate`.
annual_revenue = {} annual_revenue = {}

View File

@ -1,7 +1,7 @@
import inspect import inspect
from collections.abc import Callable from collections.abc import Callable
from functools import wraps from functools import wraps
from typing import Concatenate from typing import TYPE_CHECKING, Concatenate
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
from django.conf import settings 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_realm_and_user_from_session,
get_remote_server_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.exceptions import RemoteBillingAuthenticationError
from zerver.lib.subdomains import get_subdomain from zerver.lib.subdomains import get_subdomain
from zerver.lib.url_encoding import append_url_query_string from zerver.lib.url_encoding import append_url_query_string
from zilencer.models import RemoteRealm from zilencer.models import RemoteRealm
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
ParamT = ParamSpec("ParamT") ParamT = ParamSpec("ParamT")
@ -54,7 +56,9 @@ def self_hosting_management_endpoint(
def authenticated_remote_realm_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]: ) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
@wraps(view_func) @wraps(view_func)
def _wrapped_view_func( def _wrapped_view_func(
@ -63,6 +67,8 @@ def authenticated_remote_realm_management_endpoint(
*args: ParamT.args, *args: ParamT.args,
**kwargs: ParamT.kwargs, **kwargs: ParamT.kwargs,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RemoteRealmBillingSession
if not is_self_hosting_management_subdomain(request): # nocoverage if not is_self_hosting_management_subdomain(request): # nocoverage
return render(request, "404.html", status=404) 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( 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]: ) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
@wraps(view_func) @wraps(view_func)
def _wrapped_view_func( def _wrapped_view_func(
@ -169,6 +177,8 @@ def authenticated_remote_server_management_endpoint(
*args: ParamT.args, *args: ParamT.args,
**kwargs: ParamT.kwargs, **kwargs: ParamT.kwargs,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RemoteServerBillingSession
if not is_self_hosting_management_subdomain(request): # nocoverage if not is_self_hosting_management_subdomain(request): # nocoverage
return render(request, "404.html", status=404) return render(request, "404.html", status=404)

View File

@ -4976,7 +4976,9 @@ class StripeWebhookEndpointTest(ZulipTestCase):
"type": "checkout.session.completed", "type": "checkout.session.completed",
"data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}}, "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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_session_event_data, valid_session_event_data,
@ -4998,7 +5000,7 @@ class StripeWebhookEndpointTest(ZulipTestCase):
"data": {"object": {"object": "invoice", "id": stripe_invoice_id}}, "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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_session_event_data, valid_session_event_data,
@ -5015,7 +5017,7 @@ class StripeWebhookEndpointTest(ZulipTestCase):
) )
self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 0) 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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_session_event_data, valid_session_event_data,
@ -5026,7 +5028,7 @@ class StripeWebhookEndpointTest(ZulipTestCase):
strip_event = stripe.Event.construct_from(valid_session_event_data, stripe.api_key) strip_event = stripe.Event.construct_from(valid_session_event_data, stripe.api_key)
m.assert_called_once_with(strip_event.data.object, event) 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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_session_event_data, valid_session_event_data,
@ -5048,7 +5050,7 @@ class StripeWebhookEndpointTest(ZulipTestCase):
"data": {"object": {"object": "invoice", "id": stripe_invoice_id}}, "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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_invoice_paid_event_data, 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) 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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_invoice_paid_event_data, 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) strip_event = stripe.Event.construct_from(valid_invoice_paid_event_data, stripe.api_key)
m.assert_called_once_with(strip_event.data.object, event) 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( result = self.client_post(
"/stripe/webhook/", "/stripe/webhook/",
valid_invoice_paid_event_data, valid_invoice_paid_event_data,

View File

@ -1,5 +1,5 @@
import logging 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.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
@ -11,14 +11,6 @@ from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint, authenticated_remote_realm_management_endpoint,
authenticated_remote_server_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 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.decorator import process_as_post, require_billing_access, zulip_login_required
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
@ -29,6 +21,10 @@ from zerver.models import UserProfile
from zilencer.lib.remote_counts import MissingDataError from zilencer.lib.remote_counts import MissingDataError
from zilencer.models import RemoteRealm, RemoteZulipServer from zilencer.models import RemoteRealm, RemoteZulipServer
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
ALLOWED_PLANS_API_STATUS_VALUES = [ ALLOWED_PLANS_API_STATUS_VALUES = [
@ -49,6 +45,8 @@ def billing_page(
*, *,
success_message: str = "", success_message: str = "",
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -91,11 +89,11 @@ def billing_page(
return render(request, "corporate/billing/billing.html", context=context) return render(request, "corporate/billing/billing.html", context=context)
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def remote_realm_billing_page( def remote_realm_billing_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
success_message: str = "", success_message: str = "",
) -> HttpResponse: ) -> HttpResponse:
@ -152,11 +150,11 @@ def remote_realm_billing_page(
return render(request, "corporate/billing/billing.html", context=context) return render(request, "corporate/billing/billing.html", context=context)
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_billing_page( def remote_server_billing_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
success_message: str = "", success_message: str = "",
) -> HttpResponse: ) -> HttpResponse:
@ -246,6 +244,8 @@ def update_plan(
licenses_at_next_renewal: Json[int] | None = None, licenses_at_next_renewal: Json[int] | None = None,
schedule: Json[int] | None = None, schedule: Json[int] | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest
update_plan_request = UpdatePlanRequest( update_plan_request = UpdatePlanRequest(
status=status, status=status,
licenses=licenses, licenses=licenses,
@ -257,12 +257,12 @@ def update_plan(
return json_success(request) return json_success(request)
@authenticated_remote_realm_management_endpoint
@process_as_post @process_as_post
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def update_plan_for_remote_realm( def update_plan_for_remote_realm(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
status: Annotated[ status: Annotated[
Json[int], AfterValidator(lambda x: check_int_in(x, ALLOWED_PLANS_API_STATUS_VALUES)) 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, licenses_at_next_renewal: Json[int] | None = None,
schedule: Json[int] | None = None, schedule: Json[int] | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import UpdatePlanRequest
update_plan_request = UpdatePlanRequest( update_plan_request = UpdatePlanRequest(
status=status, status=status,
licenses=licenses, licenses=licenses,
@ -282,12 +284,12 @@ def update_plan_for_remote_realm(
return json_success(request) return json_success(request)
@authenticated_remote_server_management_endpoint
@process_as_post @process_as_post
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def update_plan_for_remote_server( def update_plan_for_remote_server(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
status: Annotated[ status: Annotated[
Json[int], AfterValidator(lambda x: check_int_in(x, ALLOWED_PLANS_API_STATUS_VALUES)) 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, licenses_at_next_renewal: Json[int] | None = None,
schedule: Json[int] | None = None, schedule: Json[int] | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import UpdatePlanRequest
update_plan_request = UpdatePlanRequest( update_plan_request = UpdatePlanRequest(
status=status, status=status,
licenses=licenses, licenses=licenses,
@ -307,14 +311,19 @@ def update_plan_for_remote_server(
return json_success(request) return json_success(request)
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_deactivate_page( def remote_server_deactivate_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
confirmed: Literal[None, "true"] = None, confirmed: Literal[None, "true"] = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import (
ServerDeactivateWithExistingPlanError,
do_deactivate_remote_server,
)
if request.method not in ["GET", "POST"]: # nocoverage if request.method not in ["GET", "POST"]: # nocoverage
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render from django.shortcuts import render
@ -8,17 +9,14 @@ from corporate.lib.decorator import (
authenticated_remote_server_management_endpoint, authenticated_remote_server_management_endpoint,
self_hosting_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.decorator import require_organization_member, zulip_login_required
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models import UserProfile from zerver.models import UserProfile
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
@ -31,6 +29,8 @@ def event_status(
stripe_session_id: str | None = None, stripe_session_id: str | None = None,
stripe_invoice_id: str | None = None, stripe_invoice_id: str | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import EventStatusRequest, RealmBillingSession
event_status_request = EventStatusRequest( event_status_request = EventStatusRequest(
stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id
) )
@ -39,15 +39,17 @@ def event_status(
return json_success(request, data) return json_success(request, data)
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def remote_realm_event_status( def remote_realm_event_status(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
stripe_session_id: str | None = None, stripe_session_id: str | None = None,
stripe_invoice_id: str | None = None, stripe_invoice_id: str | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import EventStatusRequest
event_status_request = EventStatusRequest( event_status_request = EventStatusRequest(
stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id 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) return json_success(request, data)
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_event_status( def remote_server_event_status(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
stripe_session_id: str | None = None, stripe_session_id: str | None = None,
stripe_invoice_id: str | None = None, stripe_invoice_id: str | None = None,
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
from corporate.lib.stripe import EventStatusRequest
event_status_request = EventStatusRequest( event_status_request = EventStatusRequest(
stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id stripe_session_id=stripe_session_id, stripe_invoice_id=stripe_invoice_id
) )

View File

@ -25,7 +25,6 @@ from corporate.lib.activity import (
realm_support_link, realm_support_link,
realm_url_link, realm_url_link,
) )
from corporate.lib.stripe import cents_to_dollar_string
from corporate.views.support import get_plan_type_string from corporate.views.support import get_plan_type_string
from zerver.decorator import require_server_admin from zerver.decorator import require_server_admin
from zerver.lib.typed_endpoint import typed_endpoint 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: def realm_summary_table(export: bool) -> str:
from corporate.lib.stripe import cents_to_dollar_string
now = timezone_now() now = timezone_now()
query = SQL( query = SQL(

View File

@ -1,4 +1,5 @@
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING
from urllib.parse import urlencode from urllib.parse import urlencode
import orjson import orjson
@ -13,13 +14,6 @@ from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint, authenticated_remote_realm_management_endpoint,
authenticated_remote_server_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 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.context_processors import get_realm_from_request, latest_info_context
from zerver.decorator import add_google_analytics, zulip_login_required 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.lib.typed_endpoint import typed_endpoint
from zerver.models import Realm from zerver.models import Realm
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
@add_google_analytics @add_google_analytics
def apps_view(request: HttpRequest, platform: str | None = None) -> HttpResponse: def apps_view(request: HttpRequest, platform: str | None = None) -> HttpResponse:
@ -95,6 +92,8 @@ class PlansPageContext:
@add_google_analytics @add_google_analytics
def plans_view(request: HttpRequest) -> HttpResponse: def plans_view(request: HttpRequest) -> HttpResponse:
from corporate.lib.stripe import get_free_trial_days
realm = get_realm_from_request(request) realm = get_realm_from_request(request)
context = PlansPageContext( context = PlansPageContext(
is_cloud_realm=True, is_cloud_realm=True,
@ -138,8 +137,10 @@ def plans_view(request: HttpRequest) -> HttpResponse:
@add_google_analytics @add_google_analytics
@authenticated_remote_realm_management_endpoint @authenticated_remote_realm_management_endpoint
def remote_realm_plans_page( def remote_realm_plans_page(
request: HttpRequest, billing_session: RemoteRealmBillingSession request: HttpRequest, billing_session: "RemoteRealmBillingSession"
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import get_configured_fixed_price_plan_offer, get_free_trial_days
customer = billing_session.get_customer() customer = billing_session.get_customer()
context = PlansPageContext( context = PlansPageContext(
is_self_hosted_realm=True, is_self_hosted_realm=True,
@ -205,8 +206,10 @@ def remote_realm_plans_page(
@add_google_analytics @add_google_analytics
@authenticated_remote_server_management_endpoint @authenticated_remote_server_management_endpoint
def remote_server_plans_page( def remote_server_plans_page(
request: HttpRequest, billing_session: RemoteServerBillingSession request: HttpRequest, billing_session: "RemoteServerBillingSession"
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import get_configured_fixed_price_plan_offer, get_free_trial_days
customer = billing_session.get_customer() customer = billing_session.get_customer()
context = PlansPageContext( context = PlansPageContext(
is_self_hosted_realm=True, is_self_hosted_realm=True,
@ -377,6 +380,8 @@ def communities_view(request: HttpRequest) -> HttpResponse:
@zulip_login_required @zulip_login_required
def invoices_page(request: HttpRequest) -> HttpResponseRedirect: def invoices_page(request: HttpRequest) -> HttpResponseRedirect:
from corporate.lib.stripe import RealmBillingSession
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -390,7 +395,7 @@ def invoices_page(request: HttpRequest) -> HttpResponseRedirect:
@authenticated_remote_realm_management_endpoint @authenticated_remote_realm_management_endpoint
def remote_realm_invoices_page( def remote_realm_invoices_page(
request: HttpRequest, billing_session: RemoteRealmBillingSession request: HttpRequest, billing_session: "RemoteRealmBillingSession"
) -> HttpResponseRedirect: ) -> HttpResponseRedirect:
list_invoices_session_url = billing_session.get_past_invoices_session_url() list_invoices_session_url = billing_session.get_past_invoices_session_url()
return HttpResponseRedirect(list_invoices_session_url) return HttpResponseRedirect(list_invoices_session_url)
@ -398,7 +403,7 @@ def remote_realm_invoices_page(
@authenticated_remote_server_management_endpoint @authenticated_remote_server_management_endpoint
def remote_server_invoices_page( def remote_server_invoices_page(
request: HttpRequest, billing_session: RemoteServerBillingSession request: HttpRequest, billing_session: "RemoteServerBillingSession"
) -> HttpResponseRedirect: ) -> HttpResponseRedirect:
list_invoices_session_url = billing_session.get_past_invoices_session_url() list_invoices_session_url = billing_session.get_past_invoices_session_url()
return HttpResponseRedirect(list_invoices_session_url) return HttpResponseRedirect(list_invoices_session_url)
@ -414,6 +419,8 @@ def customer_portal(
tier: Json[int] | None = None, tier: Json[int] | None = None,
setup_payment_by_invoice: Json[bool] = False, setup_payment_by_invoice: Json[bool] = False,
) -> HttpResponseRedirect: ) -> HttpResponseRedirect:
from corporate.lib.stripe import RealmBillingSession
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -427,11 +434,11 @@ def customer_portal(
return HttpResponseRedirect(review_billing_information_url) return HttpResponseRedirect(review_billing_information_url)
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def remote_realm_customer_portal( def remote_realm_customer_portal(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
return_to_billing_page: Json[bool] = False, return_to_billing_page: Json[bool] = False,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
@ -444,11 +451,11 @@ def remote_realm_customer_portal(
return HttpResponseRedirect(review_billing_information_url) return HttpResponseRedirect(review_billing_information_url)
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_customer_portal( def remote_server_customer_portal(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
return_to_billing_page: Json[bool] = False, return_to_billing_page: Json[bool] = False,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,

View File

@ -16,7 +16,6 @@ from corporate.lib.activity import (
remote_installation_stats_link, remote_installation_stats_link,
remote_installation_support_link, remote_installation_support_link,
) )
from corporate.lib.stripe import cents_to_dollar_string
from zerver.decorator import require_server_admin from zerver.decorator import require_server_admin
from zerver.models.realms import get_org_type_display_name from zerver.models.realms import get_org_type_display_name
from zilencer.models import get_remote_customer_user_count 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 @require_server_admin
def get_remote_server_activity(request: HttpRequest) -> HttpResponse: def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
from corporate.lib.stripe import cents_to_dollar_string
title = "Remote servers" title = "Remote servers"
query = SQL( query = SQL(

View File

@ -32,11 +32,6 @@ from corporate.lib.remote_billing_util import (
RemoteBillingUserDict, RemoteBillingUserDict,
get_remote_server_and_user_from_session, get_remote_server_and_user_from_session,
) )
from corporate.lib.stripe import (
BILLING_SUPPORT_EMAIL,
RemoteRealmBillingSession,
RemoteServerBillingSession,
)
from corporate.models import ( from corporate.models import (
CustomerPlan, CustomerPlan,
get_current_plan_by_customer, 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 This is the endpoint accessed via the billing_access_url, generated by
remote_realm_billing_entry entry. remote_realm_billing_entry entry.
""" """
from corporate.lib.stripe import RemoteRealmBillingSession
if request.method not in ["GET", "POST"]: if request.method not in ["GET", "POST"]:
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])
tos_consent_given = tos_consent == "true" tos_consent_given = tos_consent == "true"
@ -375,6 +372,8 @@ def remote_realm_billing_confirm_email(
a fully authenticated session. 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) identity_dict = get_identity_dict_from_signed_access_token(signed_billing_access_token)
try: try:
remote_server = get_remote_server_by_uuid(identity_dict["remote_server_uuid"]) 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. a fully authenticated session.
""" """
from corporate.lib.stripe import BILLING_SUPPORT_EMAIL
try: try:
remote_server, remote_billing_user = get_remote_server_and_user_from_session( remote_server, remote_billing_user = get_remote_server_and_user_from_session(
request, server_uuid=server_uuid 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. 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"]: if request.method not in ["GET", "POST"]:
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from pydantic import Json from pydantic import Json
@ -7,21 +8,21 @@ from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint, authenticated_remote_realm_management_endpoint,
authenticated_remote_server_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.decorator import require_billing_access, require_organization_member
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models import UserProfile from zerver.models import UserProfile
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
@require_billing_access @require_billing_access
def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse: def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession
billing_session = RealmBillingSession(user) billing_session = RealmBillingSession(user)
session_data = billing_session.create_card_update_session() session_data = billing_session.create_card_update_session()
return json_success( return json_success(
@ -32,7 +33,7 @@ def start_card_update_stripe_session(request: HttpRequest, user: UserProfile) ->
@authenticated_remote_realm_management_endpoint @authenticated_remote_realm_management_endpoint
def start_card_update_stripe_session_for_remote_realm( def start_card_update_stripe_session_for_remote_realm(
request: HttpRequest, billing_session: RemoteRealmBillingSession request: HttpRequest, billing_session: "RemoteRealmBillingSession"
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
session_data = billing_session.create_card_update_session() session_data = billing_session.create_card_update_session()
return json_success( return json_success(
@ -43,7 +44,7 @@ def start_card_update_stripe_session_for_remote_realm(
@authenticated_remote_server_management_endpoint @authenticated_remote_server_management_endpoint
def start_card_update_stripe_session_for_remote_server( def start_card_update_stripe_session_for_remote_server(
request: HttpRequest, billing_session: RemoteServerBillingSession request: HttpRequest, billing_session: "RemoteServerBillingSession"
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
session_data = billing_session.create_card_update_session() session_data = billing_session.create_card_update_session()
return json_success( return json_success(
@ -61,6 +62,8 @@ def start_card_update_stripe_session_for_realm_upgrade(
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
tier: Json[int], tier: Json[int],
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession
billing_session = RealmBillingSession(user) billing_session = RealmBillingSession(user)
session_data = billing_session.create_card_update_session_for_upgrade( session_data = billing_session.create_card_update_session_for_upgrade(
manual_license_management, tier manual_license_management, tier
@ -71,11 +74,11 @@ def start_card_update_stripe_session_for_realm_upgrade(
) )
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def start_card_update_stripe_session_for_remote_realm_upgrade( def start_card_update_stripe_session_for_remote_realm_upgrade(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
tier: Json[int], tier: Json[int],
@ -89,11 +92,11 @@ def start_card_update_stripe_session_for_remote_realm_upgrade(
) )
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def start_card_update_stripe_session_for_remote_server_upgrade( def start_card_update_stripe_session_for_remote_server_upgrade(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
tier: Json[int], tier: Json[int],

View File

@ -1,3 +1,5 @@
from typing import TYPE_CHECKING
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
@ -6,19 +8,18 @@ from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint, authenticated_remote_realm_management_endpoint,
authenticated_remote_server_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.decorator import require_organization_member, zulip_login_required
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.models import UserProfile from zerver.models import UserProfile
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
@zulip_login_required @zulip_login_required
def sponsorship_page(request: HttpRequest) -> HttpResponse: def sponsorship_page(request: HttpRequest) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -33,7 +34,7 @@ def sponsorship_page(request: HttpRequest) -> HttpResponse:
@authenticated_remote_realm_management_endpoint @authenticated_remote_realm_management_endpoint
def remote_realm_sponsorship_page( def remote_realm_sponsorship_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
context = billing_session.get_sponsorship_request_context() context = billing_session.get_sponsorship_request_context()
if context is None: if context is None:
@ -47,7 +48,7 @@ def remote_realm_sponsorship_page(
@authenticated_remote_server_management_endpoint @authenticated_remote_server_management_endpoint
def remote_server_sponsorship_page( def remote_server_sponsorship_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
context = billing_session.get_sponsorship_request_context() context = billing_session.get_sponsorship_request_context()
if context is None: if context is None:
@ -63,6 +64,8 @@ def sponsorship(
request: HttpRequest, request: HttpRequest,
user: UserProfile, user: UserProfile,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RealmBillingSession, SponsorshipRequestForm
billing_session = RealmBillingSession(user) billing_session = RealmBillingSession(user)
post_data = request.POST.copy() post_data = request.POST.copy()
form = SponsorshipRequestForm(post_data) form = SponsorshipRequestForm(post_data)
@ -73,8 +76,10 @@ def sponsorship(
@authenticated_remote_realm_management_endpoint @authenticated_remote_realm_management_endpoint
def remote_realm_sponsorship( def remote_realm_sponsorship(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
from corporate.lib.stripe import SponsorshipRequestForm
post_data = request.POST.copy() post_data = request.POST.copy()
form = SponsorshipRequestForm(post_data) form = SponsorshipRequestForm(post_data)
billing_session.request_sponsorship(form) billing_session.request_sponsorship(form)
@ -84,8 +89,10 @@ def remote_realm_sponsorship(
@authenticated_remote_server_management_endpoint @authenticated_remote_server_management_endpoint
def remote_server_sponsorship( def remote_server_sponsorship(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
) -> HttpResponse: # nocoverage ) -> HttpResponse: # nocoverage
from corporate.lib.stripe import SponsorshipRequestForm
post_data = request.POST.copy() post_data = request.POST.copy()
form = SponsorshipRequestForm(post_data) form = SponsorshipRequestForm(post_data)
billing_session.request_sponsorship(form) billing_session.request_sponsorship(form)

View File

@ -24,26 +24,6 @@ from confirmation.models import Confirmation, confirmation_url
from confirmation.settings import STATUS_USED from confirmation.settings import STATUS_USED
from corporate.lib.activity import format_optional_datetime, remote_installation_stats_link from corporate.lib.activity import format_optional_datetime, remote_installation_stats_link
from corporate.lib.billing_types import BillingModality 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 corporate.models import CustomerPlan
from zerver.actions.create_realm import do_change_realm_subdomain from zerver.actions.create_realm import do_change_realm_subdomain
from zerver.actions.realm_settings import ( from zerver.actions.realm_settings import (
@ -118,6 +98,8 @@ class DemoRequestForm(forms.Form):
@zulip_login_required @zulip_login_required
@typed_endpoint_without_parameters @typed_endpoint_without_parameters
def support_request(request: HttpRequest) -> HttpResponse: def support_request(request: HttpRequest) -> HttpResponse:
from corporate.lib.support import get_realm_support_url
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -161,6 +143,8 @@ def support_request(request: HttpRequest) -> HttpResponse:
@typed_endpoint_without_parameters @typed_endpoint_without_parameters
def demo_request(request: HttpRequest) -> HttpResponse: def demo_request(request: HttpRequest) -> HttpResponse:
from corporate.lib.stripe import BILLING_SUPPORT_EMAIL
context = { context = {
"MAX_INPUT_LENGTH": DemoRequestForm.MAX_INPUT_LENGTH, "MAX_INPUT_LENGTH": DemoRequestForm.MAX_INPUT_LENGTH,
"SORTED_ORG_TYPE_NAMES": DemoRequestForm.SORTED_ORG_TYPE_NAMES, "SORTED_ORG_TYPE_NAMES": DemoRequestForm.SORTED_ORG_TYPE_NAMES,
@ -340,7 +324,11 @@ ModifyPlan = Literal[
RemoteServerStatus = Literal["active", "deactivated"] RemoteServerStatus = Literal["active", "deactivated"]
SHARED_SUPPORT_CONTEXT = {
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_org_type_display_name": get_org_type_display_name,
"get_plan_type_name": get_plan_type_string, "get_plan_type_name": get_plan_type_string,
"dollar_amount": cents_to_dollar_string, "dollar_amount": cents_to_dollar_string,
@ -370,7 +358,15 @@ def support(
org_type: Json[NonNegativeInt] | None = None, org_type: Json[NonNegativeInt] | None = None,
max_invites: Json[NonNegativeInt] | None = None, max_invites: Json[NonNegativeInt] | None = None,
) -> HttpResponse: ) -> 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: if "success_message" in request.session:
context["success_message"] = request.session["success_message"] context["success_message"] = request.session["success_message"]
@ -692,7 +688,19 @@ def remote_servers_support(
] ]
| None = None, | None = None,
) -> HttpResponse: ) -> 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: if "success_message" in request.session:
context["success_message"] = request.session["success_message"] context["success_message"] = request.session["success_message"]

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
@ -10,14 +11,6 @@ from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint, authenticated_remote_realm_management_endpoint,
authenticated_remote_server_management_endpoint, authenticated_remote_server_management_endpoint,
) )
from corporate.lib.stripe import (
BillingError,
InitialUpgradeRequest,
RealmBillingSession,
RemoteRealmBillingSession,
RemoteServerBillingSession,
UpgradeRequest,
)
from corporate.models import CustomerPlan from corporate.models import CustomerPlan
from zerver.decorator import require_organization_member, zulip_login_required from zerver.decorator import require_organization_member, zulip_login_required
from zerver.lib.response import json_success 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 zerver.models import UserProfile
from zilencer.lib.remote_counts import MissingDataError from zilencer.lib.remote_counts import MissingDataError
if TYPE_CHECKING:
from corporate.lib.stripe import RemoteRealmBillingSession, RemoteServerBillingSession
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
@ -42,6 +38,8 @@ def upgrade(
licenses: Json[int] | None = None, licenses: Json[int] | None = None,
tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD, tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import BillingError, RealmBillingSession, UpgradeRequest
try: try:
upgrade_request = UpgradeRequest( upgrade_request = UpgradeRequest(
billing_modality=billing_modality, billing_modality=billing_modality,
@ -77,11 +75,11 @@ def upgrade(
raise BillingError(error_description, error_message) raise BillingError(error_description, error_message)
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def remote_realm_upgrade( def remote_realm_upgrade(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
billing_modality: BillingModality, billing_modality: BillingModality,
schedule: BillingSchedule, schedule: BillingSchedule,
@ -92,6 +90,8 @@ def remote_realm_upgrade(
remote_server_plan_start_date: str | None = None, remote_server_plan_start_date: str | None = None,
tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS, tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import BillingError, UpgradeRequest
try: try:
upgrade_request = UpgradeRequest( upgrade_request = UpgradeRequest(
billing_modality=billing_modality, billing_modality=billing_modality,
@ -125,11 +125,11 @@ def remote_realm_upgrade(
raise BillingError(error_description, error_message) raise BillingError(error_description, error_message)
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_upgrade( def remote_server_upgrade(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
billing_modality: BillingModality, billing_modality: BillingModality,
schedule: BillingSchedule, schedule: BillingSchedule,
@ -140,6 +140,8 @@ def remote_server_upgrade(
remote_server_plan_start_date: str | None = None, remote_server_plan_start_date: str | None = None,
tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS, tier: Json[int] = CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import BillingError, UpgradeRequest
try: try:
upgrade_request = UpgradeRequest( upgrade_request = UpgradeRequest(
billing_modality=billing_modality, billing_modality=billing_modality,
@ -182,6 +184,8 @@ def upgrade_page(
tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD, tier: Json[int] = CustomerPlan.TIER_CLOUD_STANDARD,
setup_payment_by_invoice: Json[bool] = False, setup_payment_by_invoice: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import InitialUpgradeRequest, RealmBillingSession
user = request.user user = request.user
assert user.is_authenticated assert user.is_authenticated
@ -207,17 +211,19 @@ def upgrade_page(
return response return response
@authenticated_remote_realm_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_realm_management_endpoint
def remote_realm_upgrade_page( def remote_realm_upgrade_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteRealmBillingSession, billing_session: "RemoteRealmBillingSession",
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
success_message: str = "", success_message: str = "",
tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS), tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS),
setup_payment_by_invoice: Json[bool] = False, setup_payment_by_invoice: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import InitialUpgradeRequest
billing_modality = "charge_automatically" billing_modality = "charge_automatically"
if setup_payment_by_invoice: # nocoverage if setup_payment_by_invoice: # nocoverage
billing_modality = "send_invoice" billing_modality = "send_invoice"
@ -240,17 +246,19 @@ def remote_realm_upgrade_page(
return response return response
@authenticated_remote_server_management_endpoint
@typed_endpoint @typed_endpoint
@authenticated_remote_server_management_endpoint
def remote_server_upgrade_page( def remote_server_upgrade_page(
request: HttpRequest, request: HttpRequest,
billing_session: RemoteServerBillingSession, billing_session: "RemoteServerBillingSession",
*, *,
manual_license_management: Json[bool] = False, manual_license_management: Json[bool] = False,
success_message: str = "", success_message: str = "",
tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS), tier: str = str(CustomerPlan.TIER_SELF_HOSTED_BUSINESS),
setup_payment_by_invoice: Json[bool] = False, setup_payment_by_invoice: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import InitialUpgradeRequest
billing_modality = "charge_automatically" billing_modality = "charge_automatically"
if setup_payment_by_invoice: # nocoverage if setup_payment_by_invoice: # nocoverage
billing_modality = "send_invoice" billing_modality = "send_invoice"

View File

@ -1,17 +1,11 @@
import json import json
import logging import logging
import stripe
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.views.decorators.csrf import csrf_exempt 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 corporate.models import Event, Invoice, Session
from zproject.config import get_secret from zproject.config import get_secret
@ -20,6 +14,14 @@ billing_logger = logging.getLogger("corporate.stripe")
@csrf_exempt @csrf_exempt
def stripe_webhook(request: HttpRequest) -> HttpResponse: 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", "") stripe_webhook_endpoint_secret = get_secret("stripe_webhook_endpoint_secret", "")
if ( if (
stripe_webhook_endpoint_secret and not settings.TEST_SUITE stripe_webhook_endpoint_secret and not settings.TEST_SUITE

View File

@ -48,9 +48,6 @@ from zerver.models.realms import (
from zerver.models.users import get_system_bot from zerver.models.users import get_system_bot
from zproject.backends import all_default_backend_names 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( def do_change_realm_subdomain(
realm: Realm, realm: Realm,
@ -372,6 +369,8 @@ def do_create_realm(
# Send a notification to the admin realm when a new organization registers. # Send a notification to the admin realm when a new organization registers.
if settings.CORPORATE_ENABLED: if settings.CORPORATE_ENABLED:
from corporate.lib.support import get_realm_support_url
admin_realm = get_realm(settings.SYSTEM_BOT_REALM) admin_realm = get_realm(settings.SYSTEM_BOT_REALM)
sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id) sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id)

View File

@ -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.models.users import active_user_ids, bot_owner_user_ids, get_system_bot
from zerver.tornado.django_api import send_event_on_commit 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_MESSAGES = 1000
MAX_NUM_RECENT_UNREAD_MESSAGES = 20 MAX_NUM_RECENT_UNREAD_MESSAGES = 20
@ -512,6 +508,9 @@ def do_create_user(
email_address_visibility: int | None = None, email_address_visibility: int | None = None,
add_initial_stream_subscriptions: bool = True, add_initial_stream_subscriptions: bool = True,
) -> UserProfile: ) -> UserProfile:
if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
with transaction.atomic(): with transaction.atomic():
user_profile = create_user( user_profile = create_user(
email=email, 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 parallel code path to do_create_user; e.g. it likely does not
handle preferences or default streams properly. handle preferences or default streams properly.
""" """
if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
with transaction.atomic(): with transaction.atomic():
change_user_is_active(user_profile, True) change_user_is_active(user_profile, True)
user_profile.is_mirror_dummy = False user_profile.is_mirror_dummy = False
@ -714,6 +716,8 @@ def do_reactivate_user(user_profile: UserProfile, *, acting_user: UserProfile |
bot_owner_changed = True bot_owner_changed = True
if settings.BILLING_ENABLED: if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
billing_session = RealmBillingSession(user=user_profile, realm=user_profile.realm) billing_session = RealmBillingSession(user=user_profile, realm=user_profile.realm)
billing_session.update_license_ledger_if_needed(event_time) billing_session.update_license_ledger_if_needed(event_time)

View File

@ -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.models.users import active_user_ids
from zerver.tornado.django_api import send_event_on_commit from zerver.tornado.django_api import send_event_on_commit
if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
@transaction.atomic(savepoint=False) @transaction.atomic(savepoint=False)
def do_set_realm_property( def do_set_realm_property(
@ -516,6 +513,9 @@ def do_deactivate_realm(
if realm.deactivated: if realm.deactivated:
return return
if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
with transaction.atomic(): with transaction.atomic():
realm.deactivated = True realm.deactivated = True
realm.save(update_fields=["deactivated"]) 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: def do_scrub_realm(realm: Realm, *, acting_user: UserProfile | None) -> None:
if settings.BILLING_ENABLED: if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
billing_session = RealmBillingSession(user=acting_user, realm=realm) billing_session = RealmBillingSession(user=acting_user, realm=realm)
billing_session.downgrade_now_without_creating_additional_invoices() billing_session.downgrade_now_without_creating_additional_invoices()

View File

@ -53,9 +53,6 @@ from zerver.models.users import (
) )
from zerver.tornado.django_api import send_event_on_commit 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: def do_delete_user(user_profile: UserProfile, *, acting_user: UserProfile | None) -> None:
if user_profile.realm.is_zephyr_mirror_realm: if user_profile.realm.is_zephyr_mirror_realm:
@ -324,6 +321,9 @@ def do_deactivate_user(
if not user_profile.is_active: if not user_profile.is_active:
return return
if settings.BILLING_ENABLED:
from corporate.lib.stripe import RealmBillingSession
if _cascade: if _cascade:
# We need to deactivate bots before the target user, to ensure # We need to deactivate bots before the target user, to ensure
# that a failure partway through this function cannot result # 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) maybe_enqueue_audit_log_upload(user_profile.realm)
if settings.BILLING_ENABLED and UserProfile.ROLE_GUEST in [old_value, value]: 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 = RealmBillingSession(user=user_profile, realm=user_profile.realm)
billing_session.update_license_ledger_if_needed(timezone_now()) billing_session.update_license_ledger_if_needed(timezone_now())

View File

@ -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 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 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 # We don't mark this error for translation, because it's displayed
# only to MIT users. # only to MIT users.
MIT_VALIDATION_ERROR = Markup( MIT_VALIDATION_ERROR = Markup(
@ -302,6 +298,11 @@ class HomepageForm(forms.Form):
email_is_not_mit_mailing_list(email) email_is_not_mit_mailing_list(email)
if settings.BILLING_ENABLED: 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 role = self.invited_as if self.invited_as is not None else UserProfile.ROLE_MEMBER
try: try:
check_spare_licenses_available_for_registering_new_user(realm, email, role=role) check_spare_licenses_available_for_registering_new_user(realm, email, role=role)

View File

@ -2372,7 +2372,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
with ( with (
mock.patch( mock.patch(
"zilencer.views.RemoteRealmBillingSession.get_customer", return_value=None "corporate.lib.stripe.RemoteRealmBillingSession.get_customer", return_value=None
) as m, ) as m,
mock.patch( mock.patch(
"corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses", "corporate.lib.stripe.RemoteRealmBillingSession.current_count_for_billed_licenses",
@ -2404,7 +2404,8 @@ class AnalyticsBouncerTest(BouncerTestCase):
dummy_customer = mock.MagicMock() dummy_customer = mock.MagicMock()
with ( with (
mock.patch( 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("corporate.lib.stripe.get_current_plan_by_customer", return_value=None) as m,
mock.patch( mock.patch(
@ -2425,7 +2426,8 @@ class AnalyticsBouncerTest(BouncerTestCase):
with ( with (
mock.patch( 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("corporate.lib.stripe.get_current_plan_by_customer", return_value=None),
mock.patch( mock.patch(
@ -2586,7 +2588,9 @@ class AnalyticsBouncerTest(BouncerTestCase):
"corporate.lib.stripe.RemoteServerBillingSession.get_customer", "corporate.lib.stripe.RemoteServerBillingSession.get_customer",
return_value=dummy_remote_server_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( mock.patch(
"corporate.lib.stripe.get_current_plan_by_customer", "corporate.lib.stripe.get_current_plan_by_customer",
side_effect=get_current_plan_by_customer, side_effect=get_current_plan_by_customer,
@ -2702,7 +2706,9 @@ class AnalyticsBouncerTest(BouncerTestCase):
# of a deleted realm. # of a deleted realm.
with ( with (
self.assertLogs(logger, level="WARNING") as analytics_logger, 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 # This time the logger shouldn't get triggered - because the bouncer doesn't
# include .realm_locally_deleted realms in its response. # include .realm_locally_deleted realms in its response.

View File

@ -120,10 +120,6 @@ from zproject.backends import (
password_auth_enabled, 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 @typed_endpoint
def get_prereg_key_and_redirect( def get_prereg_key_and_redirect(
@ -332,6 +328,11 @@ def registration_helper(
return redirect_to_email_login_url(email) return redirect_to_email_login_url(email)
if settings.BILLING_ENABLED: if settings.BILLING_ENABLED:
from corporate.lib.registration import (
check_spare_licenses_available_for_registering_new_user,
)
from corporate.lib.stripe import LicenseLimitError
try: try:
check_spare_licenses_available_for_registering_new_user(realm, email, role=role) check_spare_licenses_available_for_registering_new_user(realm, email, role=role)
except LicenseLimitError: except LicenseLimitError:

View File

@ -30,13 +30,6 @@ from analytics.lib.counts import (
REMOTE_INSTALLATION_COUNT_STATS, REMOTE_INSTALLATION_COUNT_STATS,
do_increment_logging_stat, 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 ( from corporate.models import (
CustomerPlan, CustomerPlan,
get_current_plan_by_customer, get_current_plan_by_customer,
@ -120,6 +113,8 @@ def deactivate_remote_server(
request: HttpRequest, request: HttpRequest,
remote_server: RemoteZulipServer, remote_server: RemoteZulipServer,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import RemoteServerBillingSession, do_deactivate_remote_server
billing_session = RemoteServerBillingSession(remote_server) billing_session = RemoteServerBillingSession(remote_server)
do_deactivate_remote_server(remote_server, billing_session) do_deactivate_remote_server(remote_server, billing_session)
return json_success(request) return json_success(request)
@ -538,6 +533,8 @@ def remote_server_notify_push(
*, *,
payload: JsonBodyPayload[RemoteServerNotificationPayload], payload: JsonBodyPayload[RemoteServerNotificationPayload],
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import get_push_status_for_remote_request
user_id = payload.user_id user_id = payload.user_id
user_uuid = payload.user_uuid user_uuid = payload.user_uuid
user_identity = UserPushIdentityCompat(user_id, 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( def update_remote_realm_data_for_server(
server: RemoteZulipServer, server_realms_info: list[RealmDataForAnalytics] server: RemoteZulipServer, server_realms_info: list[RealmDataForAnalytics]
) -> None: ) -> None:
from corporate.lib.stripe import BILLING_SUPPORT_EMAIL, RemoteRealmBillingSession
reported_uuids = [realm.uuid for realm in server_realms_info] reported_uuids = [realm.uuid for realm in server_realms_info]
all_registered_remote_realms_for_server = list(RemoteRealm.objects.filter(server=server)) all_registered_remote_realms_for_server = list(RemoteRealm.objects.filter(server=server))
already_registered_remote_realms = [ already_registered_remote_realms = [
@ -1032,6 +1031,8 @@ def get_human_user_realm_uuids(
def handle_customer_migration_from_server_to_realm( def handle_customer_migration_from_server_to_realm(
server: RemoteZulipServer, server: RemoteZulipServer,
) -> None: ) -> None:
from corporate.lib.stripe import RemoteServerBillingSession
server_billing_session = RemoteServerBillingSession(server) server_billing_session = RemoteServerBillingSession(server)
server_customer = server_billing_session.get_customer() server_customer = server_billing_session.get_customer()
if server_customer is None: if server_customer is None:
@ -1160,6 +1161,12 @@ def remote_server_post_analytics(
merge_base: Json[str] | None = None, merge_base: Json[str] | None = None,
api_feature_level: Json[int] | None = None, api_feature_level: Json[int] | None = None,
) -> HttpResponse: ) -> HttpResponse:
from corporate.lib.stripe import (
RemoteRealmBillingSession,
RemoteServerBillingSession,
get_push_status_for_remote_request,
)
# Lock the server, preventing this from racing with other # Lock the server, preventing this from racing with other
# duplicate submissions of the data # duplicate submissions of the data
server = RemoteZulipServer.objects.select_for_update().get(id=server.id) server = RemoteZulipServer.objects.select_for_update().get(id=server.id)