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 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 = {}

View File

@ -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)

View File

@ -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,

View File

@ -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"])

View File

@ -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
)

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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"])

View File

@ -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],

View File

@ -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)

View File

@ -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 = {
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"]

View File

@ -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"

View File

@ -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

View File

@ -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)

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.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)

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.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()

View File

@ -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())

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 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)

View File

@ -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.

View File

@ -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:

View File

@ -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)