mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 16:14:02 +00:00
stripe: Add a CustomerPlan for self hosted sponsored customers.
This commit is contained in:
@@ -2677,6 +2677,59 @@ class BillingSession(ABC):
|
|||||||
|
|
||||||
self.do_change_plan_type(tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, is_sponsored=False)
|
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(
|
def get_last_ledger_for_automanaged_plan_if_exists(
|
||||||
self,
|
self,
|
||||||
) -> Optional[LicenseLedger]: # nocoverage
|
) -> Optional[LicenseLedger]: # nocoverage
|
||||||
@@ -2860,6 +2913,7 @@ class RealmBillingSession(BillingSession):
|
|||||||
# This function needs to translate between the different
|
# This function needs to translate between the different
|
||||||
# formats of CustomerPlan.tier and Realm.plan_type.
|
# formats of CustomerPlan.tier and Realm.plan_type.
|
||||||
if is_sponsored:
|
if is_sponsored:
|
||||||
|
# Cloud sponsored customers don't have an active CustomerPlan.
|
||||||
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
||||||
elif tier == CustomerPlan.TIER_CLOUD_STANDARD:
|
elif tier == CustomerPlan.TIER_CLOUD_STANDARD:
|
||||||
plan_type = Realm.PLAN_TYPE_STANDARD
|
plan_type = Realm.PLAN_TYPE_STANDARD
|
||||||
@@ -3199,11 +3253,13 @@ class RemoteRealmBillingSession(BillingSession):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@transaction.atomic
|
||||||
def do_change_plan_type(
|
def do_change_plan_type(
|
||||||
self, *, tier: Optional[int], is_sponsored: bool = False
|
self, *, tier: Optional[int], is_sponsored: bool = False
|
||||||
) -> None: # nocoverage
|
) -> None: # nocoverage
|
||||||
if is_sponsored:
|
if is_sponsored:
|
||||||
plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY
|
plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY
|
||||||
|
self.add_customer_to_community_plan()
|
||||||
elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
||||||
plan_type = RemoteRealm.PLAN_TYPE_BUSINESS
|
plan_type = RemoteRealm.PLAN_TYPE_BUSINESS
|
||||||
elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||||
@@ -3581,6 +3637,7 @@ class RemoteServerBillingSession(BillingSession):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@transaction.atomic
|
||||||
def do_change_plan_type(
|
def do_change_plan_type(
|
||||||
self, *, tier: Optional[int], is_sponsored: bool = False
|
self, *, tier: Optional[int], is_sponsored: bool = False
|
||||||
) -> None: # nocoverage
|
) -> None: # nocoverage
|
||||||
@@ -3588,6 +3645,7 @@ class RemoteServerBillingSession(BillingSession):
|
|||||||
# formats of CustomerPlan.tier and RealmZulipServer.plan_type.
|
# formats of CustomerPlan.tier and RealmZulipServer.plan_type.
|
||||||
if is_sponsored:
|
if is_sponsored:
|
||||||
plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY
|
plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY
|
||||||
|
self.add_customer_to_community_plan()
|
||||||
elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
||||||
plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS
|
plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS
|
||||||
elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||||
|
@@ -5797,6 +5797,14 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
|
|||||||
billing_session.approve_sponsorship()
|
billing_session.approve_sponsorship()
|
||||||
remote_realm.refresh_from_db()
|
remote_realm.refresh_from_db()
|
||||||
self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_COMMUNITY)
|
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.
|
# Check email sent.
|
||||||
expected_message = (
|
expected_message = (
|
||||||
@@ -5920,6 +5928,14 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
|
|||||||
billing_session.approve_sponsorship()
|
billing_session.approve_sponsorship()
|
||||||
self.remote_server.refresh_from_db()
|
self.remote_server.refresh_from_db()
|
||||||
self.assertEqual(self.remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_COMMUNITY)
|
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.
|
# Check email sent.
|
||||||
expected_message = (
|
expected_message = (
|
||||||
|
@@ -7,6 +7,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<b>Plan name</b>: {{ plan_data.current_plan.name }}<br />
|
<b>Plan name</b>: {{ plan_data.current_plan.name }}<br />
|
||||||
<b>Status</b>: {{ plan_data.current_plan.get_plan_status_as_text() }}<br />
|
<b>Status</b>: {{ plan_data.current_plan.get_plan_status_as_text() }}<br />
|
||||||
|
{% if plan_data.current_plan.tier == plan_data.current_plan.TIER_SELF_HOSTED_COMMUNITY %}
|
||||||
|
<!-- Any data below doesn't makes sense for sponsored organizations. -->
|
||||||
|
{% else %}
|
||||||
{% if plan_data.is_legacy_plan %}
|
{% if plan_data.is_legacy_plan %}
|
||||||
<b>End date</b>: {{ plan_data.current_plan.end_date.strftime('%d %B %Y') }}<br />
|
<b>End date</b>: {{ plan_data.current_plan.end_date.strftime('%d %B %Y') }}<br />
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -19,3 +22,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<b>Next invoice date</b>: {{ plan_data.current_plan.next_invoice_date.strftime('%d %B %Y') }}<br />
|
<b>Next invoice date</b>: {{ plan_data.current_plan.next_invoice_date.strftime('%d %B %Y') }}<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
Reference in New Issue
Block a user