From a37354f92a245464391ae85ce7f464d334e956e7 Mon Sep 17 00:00:00 2001 From: Karl Stolley Date: Mon, 18 Dec 2023 16:57:32 -0600 Subject: [PATCH] corporate: Add a Basic plan. --- analytics/views/support.py | 1 + corporate/lib/stripe.py | 49 ++++--- ..._zulipsponsorshiprequest_requested_plan.py | 26 ++++ corporate/models.py | 6 +- corporate/views/portico.py | 2 +- help/self-hosted-billing.md | 26 ++-- .../comparison_table_self_hosted.html | 2 +- templates/corporate/plans.html | 9 +- templates/corporate/pricing_model.html | 133 ++++++++++++------ tools/linter_lib/custom_check.py | 6 +- web/styles/portico/pricing_plans.css | 36 ++++- .../commands/populate_billing_realms.py | 4 +- zilencer/models.py | 8 +- 13 files changed, 216 insertions(+), 92 deletions(-) create mode 100644 corporate/migrations/0030_alter_zulipsponsorshiprequest_requested_plan.py diff --git a/analytics/views/support.py b/analytics/views/support.py index 88a98e5882..c9c11c18f9 100644 --- a/analytics/views/support.py +++ b/analytics/views/support.py @@ -81,6 +81,7 @@ def get_plan_type_string(plan_type: int) -> str: CustomerPlan.TIER_SELF_HOSTED_LEGACY ), RemoteZulipServer.PLAN_TYPE_COMMUNITY: "Community", + RemoteZulipServer.PLAN_TYPE_BASIC: "Basic", RemoteZulipServer.PLAN_TYPE_BUSINESS: "Business", RemoteZulipServer.PLAN_TYPE_ENTERPRISE: "Enterprise", }[plan_type] diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 7ffa6624f5..65c17eed6b 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -1833,10 +1833,10 @@ class BillingSession(ABC): customer = self.get_customer() # Allow users to upgrade to business regardless of current sponsorship status. - if ( - self.is_sponsored_or_pending(customer) - and initial_upgrade_request.tier != CustomerPlan.TIER_SELF_HOSTED_BUSINESS - ): + if self.is_sponsored_or_pending(customer) and initial_upgrade_request.tier not in [ + CustomerPlan.TIER_SELF_HOSTED_BASIC, + CustomerPlan.TIER_SELF_HOSTED_BUSINESS, + ]: return f"{self.billing_session_url}/sponsorship", None remote_server_legacy_plan_end_date = self.get_formatted_remote_server_legacy_plan_end_date( @@ -1935,8 +1935,10 @@ class BillingSession(ABC): return None, context def min_licenses_for_plan(self, tier: int) -> int: - if tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: + if tier == CustomerPlan.TIER_SELF_HOSTED_BASIC: return 10 + if tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: + return 25 return 1 def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None: @@ -3258,6 +3260,8 @@ class RemoteRealmBillingSession(BillingSession): if is_sponsored: plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY self.add_customer_to_community_plan() + elif tier == CustomerPlan.TIER_SELF_HOSTED_BASIC: + plan_type = RemoteRealm.PLAN_TYPE_BASIC elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteRealm.PLAN_TYPE_BUSINESS elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: @@ -3367,8 +3371,8 @@ class RemoteRealmBillingSession(BillingSession): ) -> PlanTierChangeType: # nocoverage valid_plan_tiers = [ CustomerPlan.TIER_SELF_HOSTED_LEGACY, + CustomerPlan.TIER_SELF_HOSTED_BASIC, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, - CustomerPlan.TIER_SELF_HOSTED_PLUS, ] if ( current_plan_tier not in valid_plan_tiers @@ -3377,18 +3381,18 @@ class RemoteRealmBillingSession(BillingSession): ): return PlanTierChangeType.INVALID if ( - current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS - and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS + current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC + and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS ): return PlanTierChangeType.UPGRADE elif current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier in ( + CustomerPlan.TIER_SELF_HOSTED_BASIC, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, - CustomerPlan.TIER_SELF_HOSTED_PLUS, ): return PlanTierChangeType.UPGRADE else: - assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS - assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS + assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS + assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC return PlanTierChangeType.DOWNGRADE @override @@ -3398,6 +3402,7 @@ class RemoteRealmBillingSession(BillingSession): return True PAID_PLANS = [ + RemoteRealm.PLAN_TYPE_BASIC, RemoteRealm.PLAN_TYPE_BUSINESS, RemoteRealm.PLAN_TYPE_ENTERPRISE, ] @@ -3644,6 +3649,8 @@ class RemoteServerBillingSession(BillingSession): if is_sponsored: plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY self.add_customer_to_community_plan() + elif tier == CustomerPlan.TIER_SELF_HOSTED_BASIC: + plan_type = RemoteZulipServer.PLAN_TYPE_BASIC elif tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS: plan_type = RemoteZulipServer.PLAN_TYPE_BUSINESS elif tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY: @@ -3747,8 +3754,8 @@ class RemoteServerBillingSession(BillingSession): ) -> PlanTierChangeType: # nocoverage valid_plan_tiers = [ CustomerPlan.TIER_SELF_HOSTED_LEGACY, + CustomerPlan.TIER_SELF_HOSTED_BASIC, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, - CustomerPlan.TIER_SELF_HOSTED_PLUS, ] if ( current_plan_tier not in valid_plan_tiers @@ -3758,23 +3765,28 @@ class RemoteServerBillingSession(BillingSession): return PlanTierChangeType.INVALID if current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier in ( + CustomerPlan.TIER_SELF_HOSTED_BASIC, CustomerPlan.TIER_SELF_HOSTED_BUSINESS, - CustomerPlan.TIER_SELF_HOSTED_PLUS, ): return PlanTierChangeType.UPGRADE elif ( - current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS - and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS + current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC + and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS ): return PlanTierChangeType.UPGRADE + elif ( + current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC + and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY + ): + return PlanTierChangeType.DOWNGRADE elif ( current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY ): return PlanTierChangeType.DOWNGRADE else: - assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS - assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS + assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS + assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC return PlanTierChangeType.DOWNGRADE @override @@ -3784,6 +3796,7 @@ class RemoteServerBillingSession(BillingSession): return True PAID_PLANS = [ + RemoteZulipServer.PLAN_TYPE_BASIC, RemoteZulipServer.PLAN_TYPE_BUSINESS, RemoteZulipServer.PLAN_TYPE_ENTERPRISE, ] @@ -3887,7 +3900,7 @@ def get_price_per_license( price_map: Dict[int, Dict[str, int]] = { CustomerPlan.TIER_CLOUD_STANDARD: {"Annual": 8000, "Monthly": 800}, CustomerPlan.TIER_CLOUD_PLUS: {"Annual": 16000, "Monthly": 1600}, - # Placeholder self-hosted plan for development. + CustomerPlan.TIER_SELF_HOSTED_BASIC: {"Annual": 4200, "Monthly": 350}, CustomerPlan.TIER_SELF_HOSTED_BUSINESS: {"Annual": 8000, "Monthly": 800}, # To help with processing discount request on support page. CustomerPlan.TIER_SELF_HOSTED_LEGACY: {"Annual": 0, "Monthly": 0}, diff --git a/corporate/migrations/0030_alter_zulipsponsorshiprequest_requested_plan.py b/corporate/migrations/0030_alter_zulipsponsorshiprequest_requested_plan.py new file mode 100644 index 0000000000..31804a6371 --- /dev/null +++ b/corporate/migrations/0030_alter_zulipsponsorshiprequest_requested_plan.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.8 on 2023-12-19 11:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("corporate", "0029_session_tier"), + ] + + operations = [ + migrations.AlterField( + model_name="zulipsponsorshiprequest", + name="requested_plan", + field=models.CharField( + choices=[ + ("", "UNSPECIFIED"), + ("Community", "COMMUNITY"), + ("Basic", "BASIC"), + ("Business", "BUSINESS"), + ], + default="", + max_length=50, + ), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 2e80df4c75..0d9a57c0dc 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -270,8 +270,8 @@ class CustomerPlan(models.Model): TIER_SELF_HOSTED_BASE = 100 TIER_SELF_HOSTED_LEGACY = 101 TIER_SELF_HOSTED_COMMUNITY = 102 - TIER_SELF_HOSTED_BUSINESS = 103 - TIER_SELF_HOSTED_PLUS = 104 + TIER_SELF_HOSTED_BASIC = 103 + TIER_SELF_HOSTED_BUSINESS = 104 TIER_SELF_HOSTED_ENTERPRISE = 105 tier = models.SmallIntegerField() @@ -307,6 +307,7 @@ class CustomerPlan(models.Model): CustomerPlan.TIER_CLOUD_PLUS: "Zulip Cloud Plus", CustomerPlan.TIER_CLOUD_ENTERPRISE: "Zulip Enterprise", CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Self-managed (legacy plan)", + CustomerPlan.TIER_SELF_HOSTED_BASIC: "Zulip Basic", CustomerPlan.TIER_SELF_HOSTED_BUSINESS: "Zulip Business", CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community", }[tier] @@ -398,6 +399,7 @@ class SponsoredPlanTypes(Enum): # unspecified used for cloud sponsorship requests UNSPECIFIED = "" COMMUNITY = "Community" + BASIC = "Basic" BUSINESS = "Business" diff --git a/corporate/views/portico.py b/corporate/views/portico.py index ca947a3a82..e768f0fd9a 100644 --- a/corporate/views/portico.py +++ b/corporate/views/portico.py @@ -84,8 +84,8 @@ class PlansPageContext: billing_base_url: str = "" + tier_self_hosted_basic: int = CustomerPlan.TIER_SELF_HOSTED_BASIC tier_self_hosted_business: int = CustomerPlan.TIER_SELF_HOSTED_BUSINESS - tier_cloud_standard: int = CustomerPlan.TIER_CLOUD_STANDARD diff --git a/help/self-hosted-billing.md b/help/self-hosted-billing.md index 2a051ed424..fdf7a966df 100644 --- a/help/self-hosted-billing.md +++ b/help/self-hosted-billing.md @@ -6,25 +6,25 @@ questions about plans and billing for self-hosted organizations. Please refer to details. If you have any questions not answered here, please don't hesitate to reach out at [sales@zulip.com](mailto:sales@zulip.com). -## Business plan details and upgrades - -The Business plan is appropriate for most business organizations. It includes -unlimited access to the Mobile Push Notification Service and commercial support -for dozens of features and integrations that help businesses take full advantage -of their Zulip implementation. +## Paid plan details and upgrades For businesses with up to 10 Zulip users, the Self-managed plan is a good -option, and includes free access to the Mobile Push Notification service. For -commercial support with your installation, sign up for the Business plan, with a -minimum purchase of 10 licenses. +option, and includes free access to the Mobile Push Notification Service. -If you organization requires hands-on support, such as real-time support during +For businesses with more than 10 Zulip users, both the Basic and Business plans +include unlimited access to the Mobile Push Notification Service. + +The Business plan also includes commercial support for dozens of features and +integrations that help businesses take full advantage of their Zulip +implementation. The minimum purchase is 25 licenses. + +If your organization requires hands-on support, such as real-time support during installation and upgrades, support for advanced deployment options, custom feature development or integrations, etc., should contact [sales@zulip.com](mailto:sales@zulip.com) to discuss pricing. -Business plan discounts are available in a variety of situations; see -[below](#business-plan-discounts) for details. +Paid plan discounts are available in a variety of situations; see +[below](#paid-plan-discounts) for details. ### Upgrades for legacy customers @@ -289,7 +289,7 @@ eligibility prior to setting up a server, contact {end_tabs} -## Business plan discounts +## Paid plan discounts The following types of organizations are generally eligible for significant discounts on the Zulip Business plan. You can also contact diff --git a/templates/corporate/comparison_table_self_hosted.html b/templates/corporate/comparison_table_self_hosted.html index 14450ece4f..ff509ea59c 100644 --- a/templates/corporate/comparison_table_self_hosted.html +++ b/templates/corporate/comparison_table_self_hosted.html @@ -17,7 +17,7 @@ Features Self-managed - Community + Basic Business Enterprise diff --git a/templates/corporate/plans.html b/templates/corporate/plans.html index c72e1212c5..b3c04f74e2 100644 --- a/templates/corporate/plans.html +++ b/templates/corporate/plans.html @@ -138,7 +138,7 @@
-

Business plan discounts available

+

Paid plan discounts available

- Please reach out to sales@zulip.com if you - have any further questions or would like to discuss Enterprise pricing. + Contact sales@zulip.com + with further questions or to discuss Enterprise pricing.

diff --git a/templates/corporate/pricing_model.html b/templates/corporate/pricing_model.html index d1aafd3dc6..07e1eea097 100644 --- a/templates/corporate/pricing_model.html +++ b/templates/corporate/pricing_model.html @@ -84,7 +84,7 @@ {% if is_cloud_realm and on_free_tier and not sponsorship_pending %} - + {% if free_trial_days %} Start {{ free_trial_days }}-day free trial {% else %} @@ -160,22 +160,17 @@
-

Self-managed

+

Free

-
Free
+
Free
{% if is_self_hosted_realm and on_free_tier %} @@ -195,38 +190,73 @@
-

Community

+

Basic

-
Free
+ {% if (is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BASIC) + or (is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Basic") + or (is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BASIC)%} +
$3.50
+
+

+ /user/month billed monthly +

+
+ {% else %} +
$3.50
+
+

+ /user/month billed monthly +

+
+
+ $20/month off for the first year! +
+ {% endif %}
- {% if is_self_hosted_realm and is_sponsored %} - - - Current plan + {% if is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BASIC %} + + Upgrade is scheduled - {% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Community" %} - + {% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Business" %} + Sponsorship requested + {% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %} + + {% if free_trial_days %} + Start {{ free_trial_days }}-day free trial + {% else %} + Upgrade to Basic + {% endif %} + + {% elif is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BASIC %} + + + {% if on_free_trial %} + Current plan (free trial) + {% else %} + Current plan + {% endif %} + {% elif is_self_hosted_realm %} - - Request sponsorship + + {% if free_trial_days %} + Start {{ free_trial_days }}-day free trial + {% else %} + Upgrade to Basic + {% endif %} {% else %} - - Log in to apply + + Log in to upgrade {% endif %}
@@ -235,9 +265,10 @@
-

Business 10 users minimum

+

Business 25 users minimum