mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 16:37:23 +00:00
sponsorship: Allow remote orgs to request a plan type.
Sponsorship and billing pages modified to reflect the correctly requested sponsorship plan name. Add a line break before "Contact Zulip support".
This commit is contained in:
@@ -30,6 +30,7 @@ from corporate.models import (
|
|||||||
LicenseLedger,
|
LicenseLedger,
|
||||||
PaymentIntent,
|
PaymentIntent,
|
||||||
Session,
|
Session,
|
||||||
|
SponsoredPlanTypes,
|
||||||
ZulipSponsorshipRequest,
|
ZulipSponsorshipRequest,
|
||||||
get_current_plan_by_customer,
|
get_current_plan_by_customer,
|
||||||
get_current_plan_by_realm,
|
get_current_plan_by_realm,
|
||||||
@@ -645,6 +646,9 @@ class SponsorshipRequestForm(forms.Form):
|
|||||||
expected_total_users = forms.CharField(widget=forms.Textarea)
|
expected_total_users = forms.CharField(widget=forms.Textarea)
|
||||||
paid_users_count = forms.CharField(widget=forms.Textarea)
|
paid_users_count = forms.CharField(widget=forms.Textarea)
|
||||||
paid_users_description = forms.CharField(widget=forms.Textarea, required=False)
|
paid_users_description = forms.CharField(widget=forms.Textarea, required=False)
|
||||||
|
requested_plan = forms.ChoiceField(
|
||||||
|
choices=[(plan.value, plan.name) for plan in SponsoredPlanTypes], required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillingSession(ABC):
|
class BillingSession(ABC):
|
||||||
@@ -1654,6 +1658,7 @@ class BillingSession(ABC):
|
|||||||
customer, status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END
|
customer, status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END
|
||||||
)
|
)
|
||||||
legacy_remote_server_new_plan_name = self.get_legacy_remote_server_new_plan_name(customer)
|
legacy_remote_server_new_plan_name = self.get_legacy_remote_server_new_plan_name(customer)
|
||||||
|
is_self_hosted_billing = not isinstance(self, RealmBillingSession)
|
||||||
context = {
|
context = {
|
||||||
"plan_name": plan.name,
|
"plan_name": plan.name,
|
||||||
"has_active_plan": True,
|
"has_active_plan": True,
|
||||||
@@ -1676,8 +1681,11 @@ class BillingSession(ABC):
|
|||||||
"fixed_price": fixed_price,
|
"fixed_price": fixed_price,
|
||||||
"price_per_license": price_per_license,
|
"price_per_license": price_per_license,
|
||||||
"is_sponsorship_pending": customer.sponsorship_pending,
|
"is_sponsorship_pending": customer.sponsorship_pending,
|
||||||
|
"sponsorship_plan_name": self.get_sponsorship_plan_name(
|
||||||
|
customer, is_self_hosted_billing
|
||||||
|
),
|
||||||
"discount_percent": format_discount_percentage(customer.default_discount),
|
"discount_percent": format_discount_percentage(customer.default_discount),
|
||||||
"is_self_hosted_billing": not isinstance(self, RealmBillingSession),
|
"is_self_hosted_billing": is_self_hosted_billing,
|
||||||
"is_server_on_legacy_plan": remote_server_legacy_plan_end_date is not None,
|
"is_server_on_legacy_plan": remote_server_legacy_plan_end_date is not None,
|
||||||
"remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date,
|
"remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date,
|
||||||
"legacy_remote_server_new_plan_name": legacy_remote_server_new_plan_name,
|
"legacy_remote_server_new_plan_name": legacy_remote_server_new_plan_name,
|
||||||
@@ -2218,11 +2226,22 @@ class BillingSession(ABC):
|
|||||||
|
|
||||||
raise JsonableError(_("Pass stripe_session_id or stripe_payment_intent_id"))
|
raise JsonableError(_("Pass stripe_session_id or stripe_payment_intent_id"))
|
||||||
|
|
||||||
def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]:
|
def get_sponsorship_plan_name(
|
||||||
customer = self.get_customer()
|
self, customer: Optional[Customer], is_remotely_hosted: bool
|
||||||
is_remotely_hosted = isinstance(
|
) -> str:
|
||||||
self, (RemoteRealmBillingSession, RemoteServerBillingSession)
|
if customer is not None and customer.sponsorship_pending:
|
||||||
|
# For sponsorship pending requests, we also show the type of sponsorship requested.
|
||||||
|
# In other cases, we just show the plan user is currently on.
|
||||||
|
sponsorship_request = (
|
||||||
|
ZulipSponsorshipRequest.objects.filter(customer=customer).order_by("-id").first()
|
||||||
)
|
)
|
||||||
|
# It's possible that we marked `customer.sponsorship_pending` via support page
|
||||||
|
# without user submitting a sponsorship request.
|
||||||
|
if sponsorship_request is not None and sponsorship_request.requested_plan not in (
|
||||||
|
None,
|
||||||
|
SponsoredPlanTypes.UNSPECIFIED.value,
|
||||||
|
): # nocoverage
|
||||||
|
return sponsorship_request.requested_plan
|
||||||
|
|
||||||
# We only support sponsorships for these plans.
|
# We only support sponsorships for these plans.
|
||||||
sponsored_plan_name = CustomerPlan.name_from_tier(CustomerPlan.TIER_CLOUD_STANDARD)
|
sponsored_plan_name = CustomerPlan.name_from_tier(CustomerPlan.TIER_CLOUD_STANDARD)
|
||||||
@@ -2231,6 +2250,14 @@ class BillingSession(ABC):
|
|||||||
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY
|
CustomerPlan.TIER_SELF_HOSTED_COMMUNITY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return sponsored_plan_name
|
||||||
|
|
||||||
|
def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]:
|
||||||
|
customer = self.get_customer()
|
||||||
|
is_remotely_hosted = isinstance(
|
||||||
|
self, (RemoteRealmBillingSession, RemoteServerBillingSession)
|
||||||
|
)
|
||||||
|
|
||||||
plan_name = "Zulip Cloud Free"
|
plan_name = "Zulip Cloud Free"
|
||||||
if is_remotely_hosted:
|
if is_remotely_hosted:
|
||||||
plan_name = "Self-managed"
|
plan_name = "Self-managed"
|
||||||
@@ -2238,7 +2265,7 @@ class BillingSession(ABC):
|
|||||||
context: Dict[str, Any] = {
|
context: Dict[str, Any] = {
|
||||||
"billing_base_url": self.billing_base_url,
|
"billing_base_url": self.billing_base_url,
|
||||||
"is_remotely_hosted": is_remotely_hosted,
|
"is_remotely_hosted": is_remotely_hosted,
|
||||||
"sponsored_plan_name": sponsored_plan_name,
|
"sponsorship_plan_name": self.get_sponsorship_plan_name(customer, is_remotely_hosted),
|
||||||
"plan_name": plan_name,
|
"plan_name": plan_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2250,7 +2277,6 @@ class BillingSession(ABC):
|
|||||||
|
|
||||||
if self.is_sponsored():
|
if self.is_sponsored():
|
||||||
context["is_sponsored"] = True
|
context["is_sponsored"] = True
|
||||||
context["plan_name"] = sponsored_plan_name
|
|
||||||
|
|
||||||
if customer is not None:
|
if customer is not None:
|
||||||
plan = get_current_plan_by_customer(customer)
|
plan = get_current_plan_by_customer(customer)
|
||||||
@@ -2283,6 +2309,7 @@ class BillingSession(ABC):
|
|||||||
expected_total_users=form.cleaned_data["expected_total_users"],
|
expected_total_users=form.cleaned_data["expected_total_users"],
|
||||||
paid_users_count=form.cleaned_data["paid_users_count"],
|
paid_users_count=form.cleaned_data["paid_users_count"],
|
||||||
paid_users_description=form.cleaned_data["paid_users_description"],
|
paid_users_description=form.cleaned_data["paid_users_description"],
|
||||||
|
requested_plan=form.cleaned_data["requested_plan"],
|
||||||
)
|
)
|
||||||
sponsorship_request.save()
|
sponsorship_request.save()
|
||||||
|
|
||||||
@@ -2310,6 +2337,7 @@ class BillingSession(ABC):
|
|||||||
"expected_total_users": sponsorship_request.expected_total_users,
|
"expected_total_users": sponsorship_request.expected_total_users,
|
||||||
"paid_users_count": sponsorship_request.paid_users_count,
|
"paid_users_count": sponsorship_request.paid_users_count,
|
||||||
"paid_users_description": sponsorship_request.paid_users_description,
|
"paid_users_description": sponsorship_request.paid_users_description,
|
||||||
|
"requested_plan": sponsorship_request.requested_plan,
|
||||||
}
|
}
|
||||||
send_email(
|
send_email(
|
||||||
"zerver/emails/sponsorship_request",
|
"zerver/emails/sponsorship_request",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.2.8 on 2023-12-08 18:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("corporate", "0027_alter_zulipsponsorshiprequest_requested_by"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="zulipsponsorshiprequest",
|
||||||
|
name="requested_plan",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("", "UNSPECIFIED"), ("Community", "COMMUNITY"), ("Business", "BUSINESS")],
|
||||||
|
default="",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
@@ -384,6 +385,13 @@ class LicenseLedger(models.Model):
|
|||||||
licenses_at_next_renewal = models.IntegerField(null=True)
|
licenses_at_next_renewal = models.IntegerField(null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SponsoredPlanTypes(Enum):
|
||||||
|
# unspecified used for cloud sponsorship requests
|
||||||
|
UNSPECIFIED = ""
|
||||||
|
COMMUNITY = "Community"
|
||||||
|
BUSINESS = "Business"
|
||||||
|
|
||||||
|
|
||||||
class ZulipSponsorshipRequest(models.Model):
|
class ZulipSponsorshipRequest(models.Model):
|
||||||
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
customer = models.ForeignKey(Customer, on_delete=CASCADE)
|
||||||
requested_by = models.ForeignKey(UserProfile, on_delete=CASCADE, null=True, blank=True)
|
requested_by = models.ForeignKey(UserProfile, on_delete=CASCADE, null=True, blank=True)
|
||||||
@@ -400,3 +408,9 @@ class ZulipSponsorshipRequest(models.Model):
|
|||||||
expected_total_users = models.TextField(default="")
|
expected_total_users = models.TextField(default="")
|
||||||
paid_users_count = models.TextField(default="")
|
paid_users_count = models.TextField(default="")
|
||||||
paid_users_description = models.TextField(default="")
|
paid_users_description = models.TextField(default="")
|
||||||
|
|
||||||
|
requested_plan = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=[(plan.value, plan.name) for plan in SponsoredPlanTypes],
|
||||||
|
default=SponsoredPlanTypes.UNSPECIFIED.value,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1865,7 +1865,9 @@ class StripeTest(StripeTestCase):
|
|||||||
response = self.client_get("/sponsorship/")
|
response = self.client_get("/sponsorship/")
|
||||||
self.assert_in_success_response(
|
self.assert_in_success_response(
|
||||||
[
|
[
|
||||||
'This organization has requested sponsorship for a <a href="/plans/">Zulip Cloud Standard</a> plan. <a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.'
|
"This organization has requested sponsorship for a",
|
||||||
|
'<a href="/plans/">Zulip Cloud Standard</a>',
|
||||||
|
'plan.<br/><a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.',
|
||||||
],
|
],
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
{% if admin_access and has_active_plan %}
|
{% if admin_access and has_active_plan %}
|
||||||
{% if is_sponsorship_pending %}
|
{% if is_sponsorship_pending %}
|
||||||
<div class="alert alert-success billing-page-success" id="billing-sponsorship-pending-message-top">
|
<div class="alert alert-success billing-page-success" id="billing-sponsorship-pending-message-top">
|
||||||
This organization has requested sponsorship for a <a href="{{ billing_base_url }}/plans/">{{ plan_name }}</a> plan. <a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.
|
This organization has requested sponsorship for a
|
||||||
|
{% if sponsorship_plan_name == "Business" %}
|
||||||
|
discounted
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ billing_base_url }}/plans/">{{ sponsorship_plan_name }}</a> plan.<br/><a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if success_message %}
|
{% if success_message %}
|
||||||
|
|||||||
@@ -28,9 +28,13 @@
|
|||||||
<div class="center-block new-style">
|
<div class="center-block new-style">
|
||||||
<div class="alert alert-success sponsorship-page-success" id="sponsorship-status-success-message-top">
|
<div class="alert alert-success sponsorship-page-success" id="sponsorship-status-success-message-top">
|
||||||
{% if is_sponsored %}
|
{% if is_sponsored %}
|
||||||
Zulip is sponsoring a free <a href="{{ billing_base_url }}/plans/">{{ sponsored_plan_name }}</a> plan for this organization. 🎉
|
Zulip is sponsoring a free <a href="{{ billing_base_url }}/plans/">{{ sponsorship_plan_name }}</a> plan for this organization. 🎉
|
||||||
{% else %}
|
{% else %}
|
||||||
This organization has requested sponsorship for a <a href="{{ billing_base_url }}/plans/">{{ sponsored_plan_name }}</a> plan. <a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.
|
This organization has requested sponsorship for a
|
||||||
|
{% if sponsorship_plan_name == "Business" %}
|
||||||
|
discounted
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ billing_base_url }}/plans/">{{ sponsorship_plan_name }}</a> plan.<br/><a href="mailto:support@zulip.com">Contact Zulip support</a> with any questions or updates.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="pitch">
|
<div class="pitch">
|
||||||
@@ -107,6 +111,21 @@
|
|||||||
<label for="organization_type" class="inline-block label-title">Organization type</label>
|
<label for="organization_type" class="inline-block label-title">Organization type</label>
|
||||||
</div>
|
</div>
|
||||||
<p id="sponsorship-discount-details"></p>
|
<p id="sponsorship-discount-details"></p>
|
||||||
|
{% if is_remotely_hosted %}
|
||||||
|
<div class="input-box sponsorship-form-field">
|
||||||
|
<div class="inline-block relative">
|
||||||
|
<select name="requested_plan" id="organization-requested-plan" class="sponsorship-form-select">
|
||||||
|
<option value="Community" selected>
|
||||||
|
Community
|
||||||
|
</option>
|
||||||
|
<option value="Business">
|
||||||
|
Business (discounted)
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label for="organization-requested-plan" class="inline-block label-title">Requested plan</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="input-box sponsorship-form-field no-validation">
|
<div class="input-box sponsorship-form-field no-validation">
|
||||||
<label for="org-website" class="inline-block
|
<label for="org-website" class="inline-block
|
||||||
label-title">Organization website (if any)</label>
|
label-title">Organization website (if any)</label>
|
||||||
|
|||||||
@@ -34,4 +34,9 @@
|
|||||||
|
|
||||||
<b>Requested by</b>: {{ requested_by }} ({{ user_role }})
|
<b>Requested by</b>: {{ requested_by }} ({{ user_role }})
|
||||||
|
|
||||||
|
{% if requested_plan %}
|
||||||
|
<br /><br />
|
||||||
|
<b>Requested plan</b>: {{ requested_plan }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user