corporate: Add a Basic plan.

This commit is contained in:
Karl Stolley 2023-12-18 16:57:32 -06:00 committed by Tim Abbott
parent 3a0be097f4
commit a37354f92a
13 changed files with 216 additions and 92 deletions

View File

@ -81,6 +81,7 @@ def get_plan_type_string(plan_type: int) -> str:
CustomerPlan.TIER_SELF_HOSTED_LEGACY CustomerPlan.TIER_SELF_HOSTED_LEGACY
), ),
RemoteZulipServer.PLAN_TYPE_COMMUNITY: "Community", RemoteZulipServer.PLAN_TYPE_COMMUNITY: "Community",
RemoteZulipServer.PLAN_TYPE_BASIC: "Basic",
RemoteZulipServer.PLAN_TYPE_BUSINESS: "Business", RemoteZulipServer.PLAN_TYPE_BUSINESS: "Business",
RemoteZulipServer.PLAN_TYPE_ENTERPRISE: "Enterprise", RemoteZulipServer.PLAN_TYPE_ENTERPRISE: "Enterprise",
}[plan_type] }[plan_type]

View File

@ -1833,10 +1833,10 @@ class BillingSession(ABC):
customer = self.get_customer() customer = self.get_customer()
# Allow users to upgrade to business regardless of current sponsorship status. # Allow users to upgrade to business regardless of current sponsorship status.
if ( if self.is_sponsored_or_pending(customer) and initial_upgrade_request.tier not in [
self.is_sponsored_or_pending(customer) CustomerPlan.TIER_SELF_HOSTED_BASIC,
and initial_upgrade_request.tier != CustomerPlan.TIER_SELF_HOSTED_BUSINESS CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
): ]:
return f"{self.billing_session_url}/sponsorship", None return f"{self.billing_session_url}/sponsorship", None
remote_server_legacy_plan_end_date = self.get_formatted_remote_server_legacy_plan_end_date( 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 return None, context
def min_licenses_for_plan(self, tier: int) -> int: 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 return 10
if tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
return 25
return 1 return 1
def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None: def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None:
@ -3258,6 +3260,8 @@ class RemoteRealmBillingSession(BillingSession):
if is_sponsored: if is_sponsored:
plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY plan_type = RemoteRealm.PLAN_TYPE_COMMUNITY
self.add_customer_to_community_plan() 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: 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:
@ -3367,8 +3371,8 @@ class RemoteRealmBillingSession(BillingSession):
) -> PlanTierChangeType: # nocoverage ) -> PlanTierChangeType: # nocoverage
valid_plan_tiers = [ valid_plan_tiers = [
CustomerPlan.TIER_SELF_HOSTED_LEGACY, CustomerPlan.TIER_SELF_HOSTED_LEGACY,
CustomerPlan.TIER_SELF_HOSTED_BASIC,
CustomerPlan.TIER_SELF_HOSTED_BUSINESS, CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
CustomerPlan.TIER_SELF_HOSTED_PLUS,
] ]
if ( if (
current_plan_tier not in valid_plan_tiers current_plan_tier not in valid_plan_tiers
@ -3377,18 +3381,18 @@ class RemoteRealmBillingSession(BillingSession):
): ):
return PlanTierChangeType.INVALID return PlanTierChangeType.INVALID
if ( if (
current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
): ):
return PlanTierChangeType.UPGRADE return PlanTierChangeType.UPGRADE
elif current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier in ( 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_BUSINESS,
CustomerPlan.TIER_SELF_HOSTED_PLUS,
): ):
return PlanTierChangeType.UPGRADE return PlanTierChangeType.UPGRADE
else: else:
assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC
return PlanTierChangeType.DOWNGRADE return PlanTierChangeType.DOWNGRADE
@override @override
@ -3398,6 +3402,7 @@ class RemoteRealmBillingSession(BillingSession):
return True return True
PAID_PLANS = [ PAID_PLANS = [
RemoteRealm.PLAN_TYPE_BASIC,
RemoteRealm.PLAN_TYPE_BUSINESS, RemoteRealm.PLAN_TYPE_BUSINESS,
RemoteRealm.PLAN_TYPE_ENTERPRISE, RemoteRealm.PLAN_TYPE_ENTERPRISE,
] ]
@ -3644,6 +3649,8 @@ class RemoteServerBillingSession(BillingSession):
if is_sponsored: if is_sponsored:
plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY plan_type = RemoteZulipServer.PLAN_TYPE_COMMUNITY
self.add_customer_to_community_plan() 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: 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:
@ -3747,8 +3754,8 @@ class RemoteServerBillingSession(BillingSession):
) -> PlanTierChangeType: # nocoverage ) -> PlanTierChangeType: # nocoverage
valid_plan_tiers = [ valid_plan_tiers = [
CustomerPlan.TIER_SELF_HOSTED_LEGACY, CustomerPlan.TIER_SELF_HOSTED_LEGACY,
CustomerPlan.TIER_SELF_HOSTED_BASIC,
CustomerPlan.TIER_SELF_HOSTED_BUSINESS, CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
CustomerPlan.TIER_SELF_HOSTED_PLUS,
] ]
if ( if (
current_plan_tier not in valid_plan_tiers current_plan_tier not in valid_plan_tiers
@ -3758,23 +3765,28 @@ class RemoteServerBillingSession(BillingSession):
return PlanTierChangeType.INVALID return PlanTierChangeType.INVALID
if current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier in ( 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_BUSINESS,
CustomerPlan.TIER_SELF_HOSTED_PLUS,
): ):
return PlanTierChangeType.UPGRADE return PlanTierChangeType.UPGRADE
elif ( elif (
current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
): ):
return PlanTierChangeType.UPGRADE 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 ( elif (
current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY
): ):
return PlanTierChangeType.DOWNGRADE return PlanTierChangeType.DOWNGRADE
else: else:
assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BASIC
return PlanTierChangeType.DOWNGRADE return PlanTierChangeType.DOWNGRADE
@override @override
@ -3784,6 +3796,7 @@ class RemoteServerBillingSession(BillingSession):
return True return True
PAID_PLANS = [ PAID_PLANS = [
RemoteZulipServer.PLAN_TYPE_BASIC,
RemoteZulipServer.PLAN_TYPE_BUSINESS, RemoteZulipServer.PLAN_TYPE_BUSINESS,
RemoteZulipServer.PLAN_TYPE_ENTERPRISE, RemoteZulipServer.PLAN_TYPE_ENTERPRISE,
] ]
@ -3887,7 +3900,7 @@ def get_price_per_license(
price_map: Dict[int, Dict[str, int]] = { price_map: Dict[int, Dict[str, int]] = {
CustomerPlan.TIER_CLOUD_STANDARD: {"Annual": 8000, "Monthly": 800}, CustomerPlan.TIER_CLOUD_STANDARD: {"Annual": 8000, "Monthly": 800},
CustomerPlan.TIER_CLOUD_PLUS: {"Annual": 16000, "Monthly": 1600}, 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}, CustomerPlan.TIER_SELF_HOSTED_BUSINESS: {"Annual": 8000, "Monthly": 800},
# To help with processing discount request on support page. # To help with processing discount request on support page.
CustomerPlan.TIER_SELF_HOSTED_LEGACY: {"Annual": 0, "Monthly": 0}, CustomerPlan.TIER_SELF_HOSTED_LEGACY: {"Annual": 0, "Monthly": 0},

View File

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

View File

@ -270,8 +270,8 @@ class CustomerPlan(models.Model):
TIER_SELF_HOSTED_BASE = 100 TIER_SELF_HOSTED_BASE = 100
TIER_SELF_HOSTED_LEGACY = 101 TIER_SELF_HOSTED_LEGACY = 101
TIER_SELF_HOSTED_COMMUNITY = 102 TIER_SELF_HOSTED_COMMUNITY = 102
TIER_SELF_HOSTED_BUSINESS = 103 TIER_SELF_HOSTED_BASIC = 103
TIER_SELF_HOSTED_PLUS = 104 TIER_SELF_HOSTED_BUSINESS = 104
TIER_SELF_HOSTED_ENTERPRISE = 105 TIER_SELF_HOSTED_ENTERPRISE = 105
tier = models.SmallIntegerField() tier = models.SmallIntegerField()
@ -307,6 +307,7 @@ class CustomerPlan(models.Model):
CustomerPlan.TIER_CLOUD_PLUS: "Zulip Cloud Plus", CustomerPlan.TIER_CLOUD_PLUS: "Zulip Cloud Plus",
CustomerPlan.TIER_CLOUD_ENTERPRISE: "Zulip Enterprise", CustomerPlan.TIER_CLOUD_ENTERPRISE: "Zulip Enterprise",
CustomerPlan.TIER_SELF_HOSTED_LEGACY: "Self-managed (legacy plan)", 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_BUSINESS: "Zulip Business",
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community", CustomerPlan.TIER_SELF_HOSTED_COMMUNITY: "Community",
}[tier] }[tier]
@ -398,6 +399,7 @@ class SponsoredPlanTypes(Enum):
# unspecified used for cloud sponsorship requests # unspecified used for cloud sponsorship requests
UNSPECIFIED = "" UNSPECIFIED = ""
COMMUNITY = "Community" COMMUNITY = "Community"
BASIC = "Basic"
BUSINESS = "Business" BUSINESS = "Business"

View File

@ -84,8 +84,8 @@ class PlansPageContext:
billing_base_url: str = "" 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_self_hosted_business: int = CustomerPlan.TIER_SELF_HOSTED_BUSINESS
tier_cloud_standard: int = CustomerPlan.TIER_CLOUD_STANDARD tier_cloud_standard: int = CustomerPlan.TIER_CLOUD_STANDARD

View File

@ -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 details. If you have any questions not answered here, please don't hesitate to
reach out at [sales@zulip.com](mailto:sales@zulip.com). reach out at [sales@zulip.com](mailto:sales@zulip.com).
## Business plan details and upgrades ## Paid 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.
For businesses with up to 10 Zulip users, the Self-managed plan is a good 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 option, and includes free access to the Mobile Push Notification Service.
commercial support with your installation, sign up for the Business plan, with a
minimum purchase of 10 licenses.
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 installation and upgrades, support for advanced deployment options, custom
feature development or integrations, etc., should contact feature development or integrations, etc., should contact
[sales@zulip.com](mailto:sales@zulip.com) to discuss pricing. [sales@zulip.com](mailto:sales@zulip.com) to discuss pricing.
Business plan discounts are available in a variety of situations; see Paid plan discounts are available in a variety of situations; see
[below](#business-plan-discounts) for details. [below](#paid-plan-discounts) for details.
### Upgrades for legacy customers ### Upgrades for legacy customers
@ -289,7 +289,7 @@ eligibility prior to setting up a server, contact
{end_tabs} {end_tabs}
## Business plan discounts ## Paid plan discounts
The following types of organizations are generally eligible for significant The following types of organizations are generally eligible for significant
discounts on the Zulip Business plan. You can also contact discounts on the Zulip Business plan. You can also contact

View File

@ -17,7 +17,7 @@
<tr> <tr>
<th class="comparison-table-feature">Features</th> <th class="comparison-table-feature">Features</th>
<th>Self-<wbr />managed</th> <th>Self-<wbr />managed</th>
<th>Comm<wbr />unity</th> <th>Basic</th>
<th>Busi<wbr />ness</th> <th>Busi<wbr />ness</th>
<th>Enter<wbr />prise</th> <th>Enter<wbr />prise</th>
</tr> </tr>

View File

@ -138,7 +138,7 @@
</li> </li>
</ul> </ul>
<div class="discounted-business-plan"> <div class="discounted-business-plan">
<h3>Business plan discounts <a href="/help/self-hosted-billing#business-plan-discounts">available</a></h3> <h3>Paid plan discounts <a href="/help/self-hosted-billing#business-plan-discounts">available</a></h3>
<ul> <ul>
<li> <li>
<span><a href="/help/self-hosted-billing#business-plan-discounts">Special</a> education and non-profit pricing (100+ users)</span> <span><a href="/help/self-hosted-billing#business-plan-discounts">Special</a> education and non-profit pricing (100+ users)</span>
@ -179,7 +179,8 @@
<span><a href="/help/zulip-cloud-or-self-hosting">How do I choose between Zulip Cloud and self-hosting?</a></span> <span><a href="/help/zulip-cloud-or-self-hosting">How do I choose between Zulip Cloud and self-hosting?</a></span>
</li> </li>
<li> <li>
<span><a href="/help/self-hosted-billing#business-plan-details-and-upgrades">How do I upgrade to the Business plan?</a></span> <span><a href="/help/self-hosted-billing#business-plan-details-and-upgrades">How
do I upgrade to a paid plan?</a></span>
</li> </li>
<li> <li>
<span><a <span><a
@ -190,8 +191,8 @@
</li> </li>
</ul> </ul>
<p> <p>
Please reach out to <a href="mailto:sales@zulip.com">sales@zulip.com</a> if you Contact <a href="mailto:sales@zulip.com">sales@zulip.com</a>
have any further questions or would like to discuss Enterprise pricing. with further questions or to discuss Enterprise pricing.
</p> </p>
</div> </div>
</div> </div>

View File

@ -84,7 +84,7 @@
</div> </div>
</div> </div>
{% if is_cloud_realm and on_free_tier and not sponsorship_pending %} {% if is_cloud_realm and on_free_tier and not sponsorship_pending %}
<a href="/upgrade/?tier%3D{{ tier_cloud_standard }}" class="button upgrade-button"> <a href="/upgrade/?tier={{ tier_cloud_standard }}" class="button upgrade-button">
{% if free_trial_days %} {% if free_trial_days %}
Start {{ free_trial_days }}-day free trial Start {{ free_trial_days }}-day free trial
{% else %} {% else %}
@ -160,22 +160,17 @@
<div class="self-hosted-plan-pricing pricing-pane"> <div class="self-hosted-plan-pricing pricing-pane">
<div class="price-box" tabindex="-1"> <div class="price-box" tabindex="-1">
<div class="text-content"> <div class="text-content">
<h2>Self-managed</h2> <h2>Free</h2>
<ul class="feature-list"> <ul class="feature-list">
<li><span><a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">Mobile push notifications</a> for organizations with up to 10&nbsp;users</span></li> <li><span>Complete team chat solution, with <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">all Zulip features</a> included</span></li>
<li class="support-note"><span>Friendly <a href="/development-community/">community</a> support for:</span></li> <li><span><a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">Mobile notifications</a> for organizations with up to 10&nbsp;users</span></li>
<li><span>Tools for <a href="/why-zulip/">efficient communication</a></span></li> <li><span>Many organizations are eligible for unlimited mobile notifications on the free Community plan (see below)</span></li>
<li><span>Unlimited <a href="/help/search-for-messages">search</a> history</span></li>
<li><span>Unlimited <a href="/help/start-a-call">voice and video calls</a></span></li>
<li><span>Unlimited <a href="/integrations/">integrations</a></span></li>
<li><span>Easy <a href="https://zulip.readthedocs.io/en/stable/production/install.html">installation</a> and <a href="https://zulip.readthedocs.io/en/stable/production/upgrade.html">maintenance</a></span></li>
<li class="comparison-table-pointer"><span>And <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">much more</a>!</span></li>
</ul> </ul>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="text-content"> <div class="text-content">
<div class="standard-price-box"> <div class="standard-price-box">
<div class="price">Free</div> <div class="price no-discount">Free</div>
</div> </div>
{% if is_self_hosted_realm and on_free_tier %} {% if is_self_hosted_realm and on_free_tier %}
<span class="button current-plan-descriptor" type="button"> <span class="button current-plan-descriptor" type="button">
@ -195,38 +190,73 @@
<div class="price-box" tabindex="-1"> <div class="price-box" tabindex="-1">
<div class="text-content"> <div class="text-content">
<h2>Community</h2> <h2>Basic</h2>
<ul class="feature-list"> <ul class="feature-list">
<li class="unlimited-push-notifications"><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile push notifications</a></span></li> <li><span>Complete team chat solution, with <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">all Zulip features</a> included</span></li>
<li class="support-note"><span>Friendly <a href="/development-community/">community</a> support for:</span></li> <li><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile notifications</a></span></li>
<li><span><a href="/help/configure-authentication-methods">OAuth social login</a> (e.g., Google, GitHub)</span></li> <li><span>Support Zulip's open-source development</span></li>
<li><span><a href="/help/public-access-option">Public access option</a></span></li>
<li><span><a href="/help/moderating-open-organizations">Advanced moderation tools</a></span></li>
<li><span><a href="/help/format-your-message-using-markdown">Expressive formatting</a> (code, LaTeX)</span></li>
<li class="comparison-table-pointer"><span>And <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">much more</a>!</span></li>
</ul> </ul>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="text-content"> <div class="text-content">
<div class="standard-price-box"> <div class="standard-price-box">
<div class="price">Free</div> {% 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)%}
<div class="price no-discount"><span class="currency-symbol">$</span>3.50</div>
<div class="details">
<p>
/user/month billed monthly
</p>
</div> </div>
{% if is_self_hosted_realm and is_sponsored %} {% else %}
<a href='{{ billing_base_url }}/billing' class="button current-plan-button" type="button"> <div class="price"><span class="currency-symbol">$</span>3.50</div>
<i class="icon current-plan-icon"></i> <div class="details">
Current plan <p>
/user/month billed monthly
</p>
</div>
<div class="discount">
<b>$20/month off</b> for the first year!
</div>
{% endif %}
</div>
{% if is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BASIC %}
<a href="{{ billing_base_url }}/billing/" class="button current-plan-button">
Upgrade is scheduled
</a> </a>
{% 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" %}
<a href="{{ billing_base_url }}/billing/" class="button current-plan-button" type="button"> <a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_basic }}" class="button current-plan-button" type="button">
Sponsorship requested Sponsorship requested
</a> </a>
{% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %}
<a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_basic }}" class="button upgrade-button">
{% if free_trial_days %}
Start {{ free_trial_days }}-day free trial
{% else %}
Upgrade to Basic
{% endif %}
</a>
{% elif is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BASIC %}
<a href='{{ billing_base_url }}/billing' class="button current-plan-button" type="button">
<i class="icon current-plan-icon"></i>
{% if on_free_trial %}
Current plan (free trial)
{% else %}
Current plan
{% endif %}
</a>
{% elif is_self_hosted_realm %} {% elif is_self_hosted_realm %}
<a href="{{ billing_base_url }}/sponsorship/" class="button upgrade-button"> <a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_basic }}" class="button upgrade-button">
Request sponsorship {% if free_trial_days %}
Start {{ free_trial_days }}-day free trial
{% else %}
Upgrade to Basic
{% endif %}
</a> </a>
{% else %} {% else %}
<a href="/help/self-hosted-billing#free-community-plan" target="_blank" rel="noopener noreferrer" class="button upgrade-button"> <a href="/help/self-hosted-billing" target="_blank" rel="noopener noreferrer" class="button upgrade-button">
Log in to apply Log in to upgrade
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -235,9 +265,10 @@
<div class="price-box" tabindex="-1"> <div class="price-box" tabindex="-1">
<div class="text-content"> <div class="text-content">
<h2 class="with-fine-print">Business <small>10 users minimum</small></h2> <h2 class="with-fine-print">Business <small>25 users minimum</small></h2>
<ul class="feature-list"> <ul class="feature-list">
<li class="unlimited-push-notifications"><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile push notifications</a></span></li> <li><span>Complete team chat solution, with <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">all Zulip features</a> included</span></li>
<li><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile notifications</a></span></li>
<li class="support-note"><span>Email and chat support for:</span></li> <li class="support-note"><span>Email and chat support for:</span></li>
<li><span><a href="/help/saml-authentication">SSO with SAML, Azure AD</a></span></li> <li><span><a href="/help/saml-authentication">SSO with SAML, Azure AD</a></span></li>
<li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html">AD/LDAP user sync </a></span></li> <li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html">AD/LDAP user sync </a></span></li>
@ -250,6 +281,18 @@
<div class="bottom"> <div class="bottom">
<div class="text-content"> <div class="text-content">
<div class="standard-price-box"> <div class="standard-price-box">
<div class="standard-price-box">
{% if (is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BUSINESS)
or (is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Business")
or (is_self_hosted_realm and customer_plan and customer_plan.tier == customer_plan.TIER_SELF_HOSTED_BUSINESS)%}
<div class="price no-discount"><span class="currency-symbol">$</span>6.67</div>
<div class="details">
<p>
/user/month billed annually
or&nbsp;<b>$8</b>&nbsp;billed monthly
</p>
</div>
{% else %}
<div class="price"><span class="currency-symbol">$</span>6.67</div> <div class="price"><span class="currency-symbol">$</span>6.67</div>
<div class="details"> <div class="details">
<p> <p>
@ -257,17 +300,22 @@
or&nbsp;<b>$8</b>&nbsp;billed monthly or&nbsp;<b>$8</b>&nbsp;billed monthly
</p> </p>
</div> </div>
<div class="discount">
<b>$20/month off</b> for the first year!
</div>
{% endif %}
</div>
</div> </div>
{% if is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BUSINESS %} {% if is_legacy_server_with_scheduled_upgrade and legacy_server_new_plan.tier == legacy_server_new_plan.TIER_SELF_HOSTED_BUSINESS %}
<a href="{{ billing_base_url }}/billing/" class="button current-plan-button"> <a href="{{ billing_base_url }}/billing/" class="button current-plan-button">
Upgrade is scheduled Upgrade is scheduled
</a> </a>
{% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Business" %} {% elif is_self_hosted_realm and sponsorship_pending and requested_sponsorship_plan == "Business" %}
<a href="{{ billing_base_url }}/upgrade/?tier%3D{{ tier_self_hosted_business }}" class="button current-plan-button" type="button"> <a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_business }}" class="button current-plan-button" type="button">
Sponsorship requested Sponsorship requested
</a> </a>
{% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %} {% elif is_self_hosted_realm and on_free_tier and not sponsorship_pending %}
<a href="{{ billing_base_url }}/upgrade/?tier%3D{{ tier_self_hosted_business }}" class="button upgrade-button"> <a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_business }}" class="button upgrade-button">
{% if free_trial_days %} {% if free_trial_days %}
Start {{ free_trial_days }}-day free trial Start {{ free_trial_days }}-day free trial
{% else %} {% else %}
@ -284,7 +332,7 @@
{% endif %} {% endif %}
</a> </a>
{% elif is_self_hosted_realm %} {% elif is_self_hosted_realm %}
<a href="{{ billing_base_url }}/upgrade/?tier%3D{{ tier_self_hosted_business }}" class="button upgrade-button"> <a href="{{ billing_base_url }}/upgrade/?tier={{ tier_self_hosted_business }}" class="button upgrade-button">
{% if free_trial_days %} {% if free_trial_days %}
Start {{ free_trial_days }}-day free trial Start {{ free_trial_days }}-day free trial
{% else %} {% else %}
@ -304,7 +352,8 @@
<div class="text-content"> <div class="text-content">
<h2>Enterprise</h2> <h2>Enterprise</h2>
<ul class="feature-list"> <ul class="feature-list">
<li class="unlimited-push-notifications"><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile push notifications</a></span></li> <li><span>Complete team chat solution, with <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">all Zulip features</a> included</span></li>
<li><span>Unlimited <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">mobile notifications</a></span></li>
<li class="support-note"><span>Email, chat and phone support for:</span></li> <li class="support-note"><span>Email, chat and phone support for:</span></li>
<li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html#openid-connect">SSO with OpenID Connect</a></span></li> <li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html#openid-connect">SSO with OpenID Connect</a></span></li>
<li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html">AD/LDAP group sync</a></span></li> <li><span><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html">AD/LDAP group sync</a></span></li>

View File

@ -573,7 +573,11 @@ html_rules: List["Rule"] = [
"good_lines": ['<a href="{{variable}}">'], "good_lines": ['<a href="{{variable}}">'],
"bad_lines": ["<a href={{variable}}>"], "bad_lines": ["<a href={{variable}}>"],
# Exclude the use of URL templates from this check. # Exclude the use of URL templates from this check.
"exclude_pattern": "={code}", # Exclude the use GET parameters in URLs from this check.
"exclude_pattern": "={code}|\\?[a-z]+={|\\&[a-z]+={",
"exclude": {
"templates/corporate/pricing_model.html",
},
}, },
{ {
"pattern": " '}}", "pattern": " '}}",

View File

@ -351,6 +351,9 @@
.standard-price-box { .standard-price-box {
display: flex; display: flex;
/* Handle a discount line, when
needed. */
flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
min-height: 56px; min-height: 56px;
@ -360,18 +363,40 @@
font-size: 38px; font-size: 38px;
line-height: 1; line-height: 1;
letter-spacing: -1px; letter-spacing: -1px;
&.no-discount {
/* Pad prices that do not have
a discount (e.g., Free, or a
customer's current plan). */
padding-bottom: 25px;
}
} }
.currency-symbol { .currency-symbol {
opacity: 0.5; opacity: 0.5;
} }
.details p { .details {
flex: 1 0 0;
p {
margin: 2px 0 0; margin: 2px 0 0;
font-size: 15px; font-size: 15px;
line-height: 17px; line-height: 17px;
} }
} }
.discount {
/* Keep the discount to its own
line in the wrapping flexbox. */
flex: 0 0 100%;
text-align: center;
padding: 1px 0 6px;
margin-bottom: -5px;
border-radius: 4px 4px 0 0;
background-color: hsl(0deg 0% 85%);
}
}
} }
.additional-pricing-information { .additional-pricing-information {
@ -504,6 +529,9 @@
letter-spacing: 0.06ch; letter-spacing: 0.06ch;
padding: 8px 10px; padding: 8px 10px;
border-radius: 4px; border-radius: 4px;
/* Position relatively for discount tab */
position: relative;
z-index: 5;
} }
.button-placeholder { .button-placeholder {

View File

@ -164,7 +164,7 @@ class Command(BaseCommand):
unique_id="legacy-server-upgrade-scheduled", unique_id="legacy-server-upgrade-scheduled",
tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY, tier=CustomerPlan.TIER_SELF_HOSTED_LEGACY,
status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END, status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END,
new_plan_tier=CustomerPlan.TIER_SELF_HOSTED_PLUS, new_plan_tier=CustomerPlan.TIER_SELF_HOSTED_BASIC,
is_remote_server=True, is_remote_server=True,
), ),
CustomerProfile( CustomerProfile(
@ -441,7 +441,7 @@ def populate_remote_server(customer_profile: CustomerProfile) -> Dict[str, str]:
# This attaches stripe_customer_id to customer. # This attaches stripe_customer_id to customer.
customer = billing_session.update_or_create_stripe_customer() customer = billing_session.update_or_create_stripe_customer()
add_card_to_customer(customer) add_card_to_customer(customer)
seat_count = 10 seat_count = 30
signed_seat_count, salt = sign_string(str(seat_count)) signed_seat_count, salt = sign_string(str(seat_count))
upgrade_request = UpgradeRequest( upgrade_request = UpgradeRequest(
billing_modality="charge_automatically", billing_modality="charge_automatically",

View File

@ -64,8 +64,8 @@ class RemoteZulipServer(models.Model):
PLAN_TYPE_SELF_MANAGED = 100 PLAN_TYPE_SELF_MANAGED = 100
PLAN_TYPE_SELF_MANAGED_LEGACY = 101 PLAN_TYPE_SELF_MANAGED_LEGACY = 101
PLAN_TYPE_COMMUNITY = 102 PLAN_TYPE_COMMUNITY = 102
PLAN_TYPE_BUSINESS = 103 PLAN_TYPE_BASIC = 103
PLAN_TYPE_PLUS = 104 PLAN_TYPE_BUSINESS = 104
PLAN_TYPE_ENTERPRISE = 105 PLAN_TYPE_ENTERPRISE = 105
# The current billing plan for the remote server, similar to Realm.plan_type. # The current billing plan for the remote server, similar to Realm.plan_type.
@ -165,8 +165,8 @@ class RemoteRealm(models.Model):
PLAN_TYPE_SELF_MANAGED = 100 PLAN_TYPE_SELF_MANAGED = 100
PLAN_TYPE_SELF_MANAGED_LEGACY = 101 PLAN_TYPE_SELF_MANAGED_LEGACY = 101
PLAN_TYPE_COMMUNITY = 102 PLAN_TYPE_COMMUNITY = 102
PLAN_TYPE_BUSINESS = 103 PLAN_TYPE_BASIC = 103
PLAN_TYPE_PLUS = 104 PLAN_TYPE_BUSINESS = 104
PLAN_TYPE_ENTERPRISE = 105 PLAN_TYPE_ENTERPRISE = 105
plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED, db_index=True) plan_type = models.PositiveSmallIntegerField(default=PLAN_TYPE_SELF_MANAGED, db_index=True)