diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 3de88c354a..87f6cff19f 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -79,6 +79,11 @@ CARD_CAPITALIZATION = { "visa": "Visa", } +PAID_PLANS = [ + Realm.PLAN_TYPE_STANDARD, + Realm.PLAN_TYPE_PLUS, +] + # The version of Stripe API the billing system supports. STRIPE_API_VERSION = "2020-08-27" @@ -207,6 +212,10 @@ def check_upgrade_parameters( ) +def is_realm_on_paid_plan(realm: Realm) -> bool: + return realm.plan_type in PAID_PLANS + + # Be extremely careful changing this function. Historical billing periods # are not stored anywhere, and are just computed on the fly using this # function. Any change you make here should return the same value (or be @@ -634,6 +643,14 @@ class BillingSession(ABC): def has_billing_access(self) -> bool: pass + @abstractmethod + def on_paid_plan(self) -> bool: + pass + + @abstractmethod + def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: + pass + @catch_stripe_errors def create_stripe_customer(self) -> Customer: stripe_customer_data = self.get_data_for_stripe_customer() @@ -1567,6 +1584,34 @@ class BillingSession(ABC): raise JsonableError(_("Pass stripe_session_id or stripe_payment_intent_id")) + def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]: + context: Dict[str, Any] = {} + customer = self.get_customer() + + if customer is not None and customer.sponsorship_pending: + if self.on_paid_plan(): + return None + + context["is_sponsorship_pending"] = True + + if self.is_sponsored(): + context["is_sponsored"] = True + + if customer is not None: + plan = get_current_plan_by_customer(customer) + if plan is not None: + context["plan_name"] = plan.name + context["free_trial"] = plan.is_free_trial() + elif self.is_sponsored(): + # We don't create CustomerPlan objects for fully sponsored realms via support page. + context["plan_name"] = "Zulip Cloud Standard" + else: + # TODO: Don't hardcode this plan name. + context["plan_name"] = "Zulip Cloud Free" + + self.add_sponsorship_info_to_context(context) + return context + class RealmBillingSession(BillingSession): def __init__( @@ -1814,6 +1859,27 @@ class RealmBillingSession(BillingSession): assert self.user is not None return self.user.has_billing_access + @override + def on_paid_plan(self) -> bool: + return is_realm_on_paid_plan(self.realm) + + @override + def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: + def key_helper(d: Any) -> int: + return d[1]["display_order"] + + context.update( + realm_org_type=self.realm.org_type, + sorted_org_types=sorted( + ( + [org_type_name, org_type] + for (org_type_name, org_type) in Realm.ORG_TYPES.items() + if not org_type.get("hidden") + ), + key=key_helper, + ), + ) + class RemoteRealmBillingSession(BillingSession): # nocoverage def __init__( @@ -2015,6 +2081,16 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage # session that isn't authorized for billing access. return True + @override + def on_paid_plan(self) -> bool: + # TBD + return False + + @override + def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: + # TBD + pass + class RemoteServerBillingSession(BillingSession): # nocoverage """Billing session for pre-8.0 servers that do not yet support @@ -2211,6 +2287,16 @@ class RemoteServerBillingSession(BillingSession): # nocoverage # session that isn't authorized for billing access. return True + @override + def on_paid_plan(self) -> bool: + # TBD + return False + + @override + def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None: + # TBD + pass + def stripe_customer_has_credit_card_as_default_payment_method( stripe_customer: stripe.Customer, diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index d307f23c0a..6cf35a7e83 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -5,72 +5,28 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse -from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest -from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm +from corporate.lib.stripe import RealmBillingSession, UpdatePlanRequest, is_realm_on_paid_plan +from corporate.models import CustomerPlan, get_customer_by_realm from zerver.decorator import require_billing_access, zulip_login_required from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.validator import check_int, check_int_in, check_string -from zerver.models import Realm, UserProfile +from zerver.models import UserProfile billing_logger = logging.getLogger("corporate.stripe") -PAID_PLANS = [ - Realm.PLAN_TYPE_STANDARD, - Realm.PLAN_TYPE_PLUS, -] - - -def is_realm_on_paid_plan(realm: Realm) -> bool: - return realm.plan_type in PAID_PLANS - - -def add_sponsorship_info_to_context(context: Dict[str, Any], user_profile: UserProfile) -> None: - def key_helper(d: Any) -> int: - return d[1]["display_order"] - - context.update( - realm_org_type=user_profile.realm.org_type, - sorted_org_types=sorted( - ( - [org_type_name, org_type] - for (org_type_name, org_type) in Realm.ORG_TYPES.items() - if not org_type.get("hidden") - ), - key=key_helper, - ), - ) - @zulip_login_required @has_request_variables def sponsorship_request(request: HttpRequest) -> HttpResponse: user = request.user assert user.is_authenticated - context: Dict[str, Any] = {} - customer = get_customer_by_realm(user.realm) - if customer is not None and customer.sponsorship_pending: - if is_realm_on_paid_plan(user.realm): - return HttpResponseRedirect(reverse("billing_home")) + billing_session = RealmBillingSession(user) + context = billing_session.get_sponsorship_request_context() + if context is None: + return HttpResponseRedirect(reverse("billing_home")) - context["is_sponsorship_pending"] = True - - if user.realm.plan_type == user.realm.PLAN_TYPE_STANDARD_FREE: - context["is_sponsored"] = True - - if customer is not None: - plan = get_current_plan_by_customer(customer) - if plan is not None: - context["plan_name"] = plan.name - context["free_trial"] = plan.is_free_trial() - # We don't create CustomerPlan objects for fully sponsored realms via support page. - elif user.realm.plan_type == Realm.PLAN_TYPE_STANDARD_FREE: - context["plan_name"] = "Zulip Cloud Standard" - else: - context["plan_name"] = "Zulip Cloud Free" - - add_sponsorship_info_to_context(context, user) return render(request, "corporate/sponsorship.html", context=context)