diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index ddab91a38a..8352e02410 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -2677,6 +2677,59 @@ class BillingSession(ABC): self.do_change_plan_type(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, is_sponsored=False) + def add_customer_to_community_plan(self) -> None: + # There is no CustomerPlan for organizations on Zulip Cloud and + # they enjoy the same benefits as the Standard plan. + # For self-hosted organizations, sponsored organizations have + # a Community CustomerPlan and they have different benefits compared + # to customers on Business plan. + assert not isinstance(self, RealmBillingSession) + + customer = self.update_or_create_customer() + plan = get_current_plan_by_customer(customer) + # Only plan that can be active is legacy plan. Which is already + # ended by the support path from which is this function is called. + assert plan is None + now = timezone_now() + community_plan_params = { + "billing_cycle_anchor": now, + "status": CustomerPlan.ACTIVE, + "tier": CustomerPlan.TIER_SELF_HOSTED_COMMUNITY, + # The primary mechanism for preventing charges under this + # plan is setting a null `next_invoice_date`, but setting + # a 0 price is useful defense in depth here. + "next_invoice_date": None, + "price_per_license": 0, + "billing_schedule": CustomerPlan.BILLING_SCHEDULE_ANNUAL, + "automanage_licenses": True, + } + community_plan = CustomerPlan.objects.create( + customer=customer, + **community_plan_params, + ) + + try: + billed_licenses = self.get_billable_licenses_for_customer(customer, community_plan.tier) + except MissingDataError: + billed_licenses = 0 + + # Create a ledger entry for the community plan for tracking purposes. + # Also, since it is an active plan we need to it have at least one license ledger entry. + ledger_entry = LicenseLedger.objects.create( + plan=community_plan, + is_renewal=True, + event_time=now, + licenses=billed_licenses, + licenses_at_next_renewal=billed_licenses, + ) + community_plan.invoiced_through = ledger_entry + community_plan.save(update_fields=["invoiced_through"]) + self.write_to_audit_log( + event_type=AuditLogEventType.CUSTOMER_PLAN_CREATED, + event_time=now, + extra_data=community_plan_params, + ) + def get_last_ledger_for_automanaged_plan_if_exists( self, ) -> Optional[LicenseLedger]: # nocoverage @@ -2860,6 +2913,7 @@ class RealmBillingSession(BillingSession): # This function needs to translate between the different # formats of CustomerPlan.tier and Realm.plan_type. if is_sponsored: + # Cloud sponsored customers don't have an active CustomerPlan. plan_type = Realm.PLAN_TYPE_STANDARD_FREE elif tier == CustomerPlan.TIER_CLOUD_STANDARD: plan_type = Realm.PLAN_TYPE_STANDARD @@ -3199,11 +3253,13 @@ class RemoteRealmBillingSession(BillingSession): return customer @override + @transaction.atomic def do_change_plan_type( self, *, tier: Optional[int], is_sponsored: bool = False ) -> None: # nocoverage if is_sponsored: plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY + self.add_customer_to_community_plan() elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteRealm.PLAN_TYPE_BUSINESS elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: @@ -3581,6 +3637,7 @@ class RemoteServerBillingSession(BillingSession): return customer @override + @transaction.atomic def do_change_plan_type( self, *, tier: Optional[int], is_sponsored: bool = False ) -> None: # nocoverage @@ -3588,6 +3645,7 @@ class RemoteServerBillingSession(BillingSession): # formats of CustomerPlan.tier and RealmZulipServer.plan_type. if is_sponsored: plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY + self.add_customer_to_community_plan() elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 31a029b095..0db02c9ae6 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -5797,6 +5797,14 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): billing_session.approve_sponsorship() remote_realm.refresh_from_db() self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_COMMUNITY) + # Assert such a plan exists + CustomerPlan.objects.get( + customer=customer, + tier=CustomerPlan.TIER_SELF_HOSTED_COMMUNITY, + status=CustomerPlan.ACTIVE, + next_invoice_date=None, + price_per_license=0, + ) # Check email sent. expected_message = ( @@ -5920,6 +5928,14 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase): billing_session.approve_sponsorship() self.remote_server.refresh_from_db() self.assertEqual(self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_COMMUNITY) + # Assert such a plan exists + CustomerPlan.objects.get( + customer=customer, + tier=CustomerPlan.TIER_SELF_HOSTED_COMMUNITY, + status=CustomerPlan.ACTIVE, + next_invoice_date=None, + price_per_license=0, + ) # Check email sent. expected_message = ( diff --git a/templates/analytics/current_plan_details.html b/templates/analytics/current_plan_details.html index c25b099524..818cf7f8c9 100644 --- a/templates/analytics/current_plan_details.html +++ b/templates/analytics/current_plan_details.html @@ -7,6 +7,9 @@ {% endif %} Plan name: {{ plan_data.current_plan.name }}
Status: {{ plan_data.current_plan.get_plan_status_as_text() }}
+{% if plan_data.current_plan.tier == plan_data.current_plan.TIER_SELF_HOSTED_COMMUNITY %} + +{% else %} {% if plan_data.is_legacy_plan %} End date: {{ plan_data.current_plan.end_date.strftime('%d %B %Y') }}
{% else %} @@ -19,3 +22,4 @@ {% endif %} Next invoice date: {{ plan_data.current_plan.next_invoice_date.strftime('%d %B %Y') }}
{% endif %} +{% endif %}